授权与合规声明本文全部操作对象均为自建隔离靶场本机容器或隔离虚拟机涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款须承担相应法律责任。本文只讲环境配置、版本对照与靶场隔离不含任何攻击步骤、利用载荷与绕过手法请勿将文中环境指向任何非自有系统。一、“停不掉”其实是三件事1.1 先把这句话拆成三类在靶场里改完配置、重启服务遇到起不来的情况绝大多数人第一句话是“端口没放出来”。这句话听起来像一件事实际上至少是三件事的合称而且三件事的观察位置完全不同。第一件是进程还在。服务的进程没有真正退出监听套接字还握在它手里新进程去绑同一个地址和端口当然失败。这一层的证据在进程表里不在连接表里。第二件是进程退了记录还在。连接关闭以后协议栈不会立刻把这条连接的四元组丢掉先关闭的那一端要留一段时间这就是 TIME-WAIT。这时候你看进程表是干净的看连接表还有东西于是很容易误判成“进程没死”。第三件是规则层面不允许。绑定失败不一定因为有人占着也可能因为地址校验这一关没过——套接字选项有没有在绑定之前设上会直接改变哪些绑定是允许的。三件事混在一起排查最常见的结局是得出“重启一下就好了”或者“等一会儿就好了”。这类经验在单机上碰巧有效换一台机器、换一个场景就失效因为它没有指向任何一个具体的层。现象真正的原因在哪一层证据看哪里本文是否展开服务停了进程还在跑进程层进程表、监听套接字只在第七章给一个观察动作进程没了连接表里还有残留协议栈的连接层连接表状态与四元组第二至五章进程没了、连接也干净还是绑不上绑定规则层代码里 bind 之前设的选项第六章以上都不是绑定之外的其他原因不在本文范围否第四行是有意留白的。同一类“起不来”还有别的成因和连接关闭没有关系本文不碰。1.2 判断“是谁留下的”先看谁先关规范把这件事写得很直白。RFC 9293截至 2026-10-20 的现行版本第 3.5 节的原文是When a connection is closed actively, it MUST linger in the TIME-WAIT state for a time 2xMSL (Maximum Segment Lifetime) (MUST-13).这句话的主语是关键主动关闭的那一方。所以看到一条 TIME-WAIT 记录第一个要回答的问题不是“怎么把它清掉”而是“这条连接是谁先发的关闭”。短链接触发的场景里通常是请求方先关服务端因为超时、因为业务逻辑主动断开、因为进程退出而发出的关闭服务端就是主动关闭方。同一个服务端口上这两种情况下被占住的东西并不是同一个第五章会把这件事拆开。1.3 本文的范围以及一句必要的声明需要先把范围说清楚TCP 状态机全貌不在本文范围。那些状态各自什么时候进、什么时候出是一整套东西本文不铺开需要那一层的读者直接读 RFC 9293 第 3.4.2 节。本文只引用 TIME-WAIT 一个状态的定义因为“端口为什么还没放出来”这个问题只和这一个状态有关。只讲一个状态不是偷懒。相反把注意力收窄到一个状态上才有可能把这一个状态讲透它为什么存在、它存在多久、它占的是谁的资源、以及哪些做法是误解。二、连接关闭是四次交互2.1 为什么是四次而不是两次TCP 是全双工的。同一条连接上有两个方向的数据流各自独立地开始、独立地结束。这一点决定了关闭不能一次说完。一端说“我没有数据要发了”只表示它到自己这个方向结束了对端可能还有没发完的数据。所以关闭要分成四步一端发出结束请求另一端确认等另一端自己也没数据了再发出它那一侧的结束请求最后由最初那一端确认。如果硬要压缩成两次就得让“我不发了”和“你也不要发了”绑在一起那等于单方面掐断对端还在传输的数据。所以四次的本质不是协议啰嗦而是两个方向必须分别确认。2.2 四次交互各自“清了什么”次序方向携带的语义清掉的是什么第一次主动关闭方 → 对端本方向不再发送数据主动关闭方这一侧的发送通道第二次对端 → 主动关闭方收到上面的声明一个悬而未决的确认第三次对端 → 主动关闭方对端这一侧也结束对端这一侧的发送通道第四次主动关闭方 → 对端确认对端的结束最后一个悬而未决的确认四次走完双方的发送通道都关了。注意一个容易被忽略的点前两次和后两次之间是可以有间隔的。第二次确认发出去以后对端可能还有数据要传这段间隔在协议里是允许的连接处于一种“半关”的中间态。这就是为什么抓包时会看到关闭过程拖得很长——不是网络慢是有一端确实还有东西没发完。2.3 第四次发出之后为什么不能立刻消失这是整篇文章的转折点值得停一下。第四次的那个确认发出去主动关闭方在逻辑上已经没事可做了。但它不能立刻把这条连接的所有痕迹抹掉原因在于这个确认本身可能丢。如果它丢了对端收不到就会认为第三次的结束请求没被确认于是重发。重发的这个报文到达时主动关闭方如果已经把这条连接忘干净了就会用一个全新的上下文去接一个旧连接的报文——序列号对不上轻则回一个复位重则把新旧两条连接的数据搅在一起。RFC 9293 第 3.4.3 节把底层原因写得很清楚every segment emitted occupies one or more sequence numbers in the sequence space, and the numbers occupied by a segment are “busy” or “in use” until MSL seconds have passed.也就是说一个报文占掉的序列号在 MSL 秒之内都算“仍在占用”。这段时间里同一对地址和端口之间如果起一条新连接它的序列号空间就有可能和旧连接残留的报文重叠。TIME-WAIT 就是用来把这段重叠窗口挡住的。所以 TIME-WAIT 不是“关不干净”而是关得太干净反而会出问题。三、TIME-WAIT 为什么必须存在3.1 定义里其实写了两件事RFC 9293 第 3.4.2 节给这个状态的定义是TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP peer received the acknowledgment of its connection termination request and to avoid new connections being impacted by delayed segments from previous connections.一句话里塞了两个职责值得逐段拆第一个职责是“确保对端收到了它那次终止请求的确认”。对应 2.3 讲的确认丢失问题——等够时间即使确认丢了、对端重发了重发的报文也会在这段等待期内被正确处理。第二个职责是“避免新连接被上一条连接的延迟报文影响”。对应序列号空间的重叠问题——等够时间旧报文要么已经到达并被丢弃要么已经在网络中消失。两个职责对应两个不同的故障场景职责对应的问题不等待的后果确保对端收到终止确认第四次交互的确认丢失、对端重发重发的报文打到一条已不存在的连接上避免旧报文影响新连接延迟报文在路径上滞留旧报文被新连接误认为自己的数据3.2 两个职责为什么不能用别的办法替代有人会想既然只是等那能不能让新连接用一个完全不同的序列号起点绕开重叠规范其实考虑过这件事。RFC 9293 第 3.5 节那一段紧接着还有一句TIME-WAIT 状态下可以接受对端发来的新 SYN 直接重建连接但前提是两条新连接用的初始序列号必须大于上一条连接用过的最大序列号并且如果这个 SYN 被判定为旧连接的重复报文就要退回 TIME-WAIT。这两条前提说明了一件事跳过等待是有条件的条件本身就是在防重叠。它不是“允许抢占”而是“在能证明不会重叠的前提下提前放行”。证明不了就老实等。这也是本文不写“怎么让 TIME-WAIT 消失”的原因。规范给了提前重建的路径但走这条路要做序列号比较那是协议实现内部的事不是使用者能拍脑袋决定的。3.3 它是“等”不是“卡”初学者看到连接表里一堆 TIME-WAIT第一反应是“出问题了”。这个判断需要纠正。TIME-WAIT 有三个特征每一个都和“故障”相反它是有界的等待时长由规范给定到点自动结束它是自动的不需要任何人工干预它是局部的只锁住那一个四元组不影响别的连接。对照一下真正需要处理的情况进程残留不会自己消失你不处理它就一直占着绑定规则不满足不会随时间好转你不改代码就一直绑不上。TIME-WAIT 和这两者最本质的区别就是它会自己走完。所以正确的态度不是“消灭它”而是“知道它在那儿、知道它占的是什么、知道它什么时候走”。四、2×MSL 这个数怎么来的4.1 MSL 是一个假设值不是一个测量值MSL 是 Maximum Segment Lifetime 的缩写指一个报文在网络中能存活的最长时间。这里要特别强调“最长”两个字是规范给的上界假设不是任何一台设备测出来的实际值。真实网络里一个报文活多久取决于经过多少跳、每跳怎么排队、有没有环路没法给出统一数字。所以规范不去测它而是规定一个值让所有实现按同一个假设去算等待时长。理解这一点后面很多困惑就自然消解了你在这台机器上量出来的 TIME-WAIT 时长和规范写的数字对不上不代表谁错了只代表实现选了自己的取值。4.2 规范取 2 分钟于是等待是 4 分钟RFC 9293 第 3.4.2 节的原文是For this specification the MSL is taken to be 2 minutes. This is an engineering choice, and may be changed if experience indicates it is desirable to do so.原文自己就承认了这是“工程选择”engineering choice而且明确说可以改。这句话很重要它把“4 分钟”从一个铁律降格成了一个默认约定。按这个取值算主动关闭方停留在 TIME-WAIT 的时长 2 × MSL 4 分钟。这是规范取值不是本机实测值。实际实现可能不同不同系统的默认取值也可能不同。写文章时把这两者混为一谈是这类内容最常见的失真来源。量取值性质能不能改MSL2 分钟规范给定的工程选择规范原文说可以视经验而定2×MSL4 分钟由上一行直接算出随上一行变化某台机器上的实际等待时长未在本文实测实现细节不在本文范围4.3 为什么偏偏是两倍一倍不够两倍刚好原因在方向上。考虑最坏情况主动关闭方发出的最后一个确认在路径上走了接近 MSL 才丢对端等不到确认重发它的结束请求这个重发又要走接近 MSL 才到达。两段加起来就是 2×MSL。等待期必须覆盖这个最坏情况否则重发的报文会在等待期之后到达正好撞上新连接。再叠上 2.3 里那条“序列号在 MSL 秒内都算占用”两个 MSL 的窗口刚好把旧连接占用过的序列号全部熬过去。所以两倍不是拍出来的是两个方向各一次的最坏延迟相加。五、占的是哪个端口5.1 一条连接由四元组确定一条 TCP 连接由四个要素唯一确定源地址、源端口、目的地址、目的端口。TIME-WAIT 锁住的就是这整个四元组而不是单独一个端口号。这一条是理解“端口复用”的前提同一时刻同一个本地端口完全可以出现在多条连接里只要对端地址或对端端口不同。所以“端口被占了”这个说法严格讲应该是“这个四元组被占了”。反过来说一个监听端口能同时接受很多连接靠的也正是四元组不同——每个进来的连接对端地址端口都不一样。5.2 谁主动关决定被占住的是谁主动关闭方被 TIME-WAIT 锁住的四元组表现出来像什么请求方客户端客户端临时端口 服务端地址端口客户端短时间内反复连同一服务临时端口吃紧服务端服务端端口 该客户端地址端口服务端重启时旧连接的残留挂在服务端口上第二种情况正是“服务停了端口还没放出来”这个说法的来源服务进程已经退出但它在退出前主动断开的那些连接四元组还挂在服务端口这一侧。这里有一条必须标清楚的边界。服务端主动关闭时请求方那一侧是否也会进入 TIME-WAIT属于具体实现的细节规范只对主动关闭方提了要求。这一点本文没有逐字核到内核实现标为待验证不作为结论使用。你如果在自己的环境里观察到两端都有残留也不必惊讶先按待验证处理。5.3 本机实测临时端口是一个固定区间上面反复出现“临时端口”这个词它到底是哪一段在 macOS 上可以直接问系统。本机实测macOS2026-10-20sysctlnet.inet.ip.portrange.first net.inet.ip.portrange.lastnet.inet.ip.portrange.first: 49152 net.inet.ip.portrange.last: 65535这段区间就是临时端口池49152 到 65535首尾都算一共 16384 个。再看连接表里的 TIME-WAIT 长什么样。同样是本机实测macOS2026-10-20netstat-an-ptcp|grepTIME_WAIT|head-4tcp4 0 0 127.0.0.1.56356 127.0.0.1.9229 TIME_WAIT tcp4 0 0 127.0.0.1.56357 127.0.0.1.9229 TIME_WAIT tcp4 0 0 127.0.0.1.56358 127.0.0.1.9229 TIME_WAIT tcp4 0 0 127.0.0.1.56359 127.0.0.1.9229 TIME_WAIT这四行信息量很大逐列读一遍左侧地址是 127.0.0.1 加一个 56356 起的端口全部落在 49152–65535 这个区间里——这就是临时端口。右侧是 127.0.0.1 加 9229四条完全一样——这是被访问的那个服务端口。状态列都是 TIME_WAIT。结论很直接这四条记录锁住的是本地临时端口9229 那个服务端口并没有被锁。服务端口只要能继续监听新的连接照样进得来。真正被消耗的是临时端口池里的四个名额。同一份统计里TIME_WAIT 这一项的计数是 37。这个数字本身说明不了任何问题——37 相对于 16384 的池子占比很小而且它是本机某一时刻的快照下一秒就变了。只有当你看到这个数持续上涨、并且和“新建连接失败”同时出现时它才值得追。完整版环境对照表本文用到的 macOS 与 Linux 在连接表命令、临时端口区间上的差异我整理成了一张对照表和本文第五章的观察口径一一对应。放在资料包里扫码即可获取六、两个 socket 选项的差别6.1 SO_REUSEADDR 管的是“校验时允许复用”到了这一层问题从“等多久”变成了“允许不允许”。很多人听说过一个选项叫SO_REUSEADDR也听说过“设了它就能复用端口”但这句话省掉了最关键的后半段。Linux man-pages 的socket(7)对这个选项的原文是Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses. For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address. When the listening socket is bound to INADDR_ANY with a specific port then it is not possible to bind to this port for any local address.拆成三层看第一层它改的是校验规则。绑定的时候内核要做一次地址校验这个选项让校验放宽允许复用本地地址。第二层“except when there is an active listening socket bound to the address”。这句话是硬边界只要那个地址上还有一个活跃的监听套接字就绑不上。第三层如果监听套接字绑在 INADDR_ANY 加某个具体端口上那么任何本地地址都绑不到这个端口。所以必须把话说死SO_REUSEADDR不是“可以抢占已被监听的端口”。它放宽的是“这个地址刚被用过、还带着残留”这类情形不是“这个地址现在正有人听着”。这两件事在初学者嘴里经常被合并成一句“开了就能复用”而恰恰是这半步之差让很多人在服务真的还在跑的时候误以为加个选项就能顶上去。6.2 SO_REUSEPORT 才是“多个套接字绑同一地址”真正允许“多个套接字绑同一个地址”的是另一个选项。同一份文档里对SO_REUSEPORTLinux 3.9 起的原文是Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address. This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket. To prevent port hijacking, all of the processes binding to the same address must have the same effective UID.三个要点它才管“同一地址多绑”。这是字面意义上的共用同一个地址和端口。每一个套接字都要设包括第一个。这一点常被漏掉只给后来的套接字设、第一个不设是不成立的。选项必须在调用绑定之前设好。同一地址上的所有进程必须有效 UID 相同。原文给的理由是防止端口被劫持port hijacking——如果没有这条限制任何用户都能往别人的服务端口上挂一个套接字分走流量。6.3 一张对照表加三个常见误读对比项SO_REUSEADDRSO_REUSEPORT改的是什么绑定时的地址校验规则是否允许多个套接字绑同一地址有活跃监听时能否绑上不能能前提是各方都设了设置范围目标套接字每一个套接字含第一个有没有身份限制原文未提有效 UID 必须相同常见误读“能抢别人的端口”“设了就会自动分流”三个误读逐个纠正误读一SO_REUSEADDR能抢占正在监听的端口。不能。原文的 except 从句写得明明白白有活跃监听套接字时绑不上。误读二两个选项是一回事随便设一个。不是。一个放宽校验一个允许多绑作用点完全不同。误读三SO_REUSEPORT设上就等于负载均衡。原文只说允许多个套接字绑同一地址没说内核怎么在这些套接字之间分配新连接。分配策略不在本文范围也不该由这一个选项反推。理解了这一层“服务停了端口还没放出来”就有了完整的解释路径如果 TIME-WAIT 的残留挂在临时端口上服务端口本身不受影响如果绑定失败发生在服务端口上先分清是进程还在还是选项没设对而不是一上来就去动系统参数。七、自查顺序7.1 五步每一步只回答一个问题把前面六章压成一条可执行的路径。每一步只回答一个问题答完再走下一步不要跳。步骤问什么看哪里判据一这条连接是谁先关的业务逻辑、抓包、日志时序主动关闭方才可能留下 TIME-WAIT二残留挂在哪个四元组上连接表左侧端口是临时端口还是服务端口三服务端口上还有没有活跃监听连接表里的监听项有活跃监听SO_REUSEADDR 帮不上四进程是否真的退出了进程表进程在问题不在协议栈五选项是在绑定之前设的吗源码里绑定的前几行设晚了等于没设第二步在 Linux 上可以用ss按状态过滤只看 time-wait不必把整张表打出来⚠️代码待验证ss-tanstate time-wait第三步要确认某个端口上到底有没有监听。macOS 上用lsof⚠️代码待验证lsof-nP-iTCP:9229-sTCP:LISTEN这两条都是只读观察不改任何状态。之所以标待验证是因为它们的效果依赖你机器上的工具版本与权限本文没有在写稿环境里逐条跑过。7.2 三个需要改掉的判断习惯第一个习惯把 TIME-WAIT 当泄漏。它是有界、自动、局部的等待不是资源泄漏。看到它先问“这是谁的主动关闭”而不是“怎么清掉”。第二个习惯以为加个选项就能顶上去。SO_REUSEADDR在活跃监听面前无效SO_REUSEPORT有它自己的前提条件两个都不能用来“抢”端口。第三个习惯把 4 分钟当硬编码。它是规范按 MSL2 分钟算出来的取值规范原文自己说了这是工程选择、可以改。所以你在自己环境里量到的数字和 4 分钟不同是正常的。7.3 下一步该看哪里如果你手上正在排查这个问题按这个顺序看三处就够了先看连接表的四元组确认被占的是临时端口还是服务端口再看那个端口上有没有活跃监听最后回到代码确认选项是不是在绑定之前设的。三处看完问题一定落在其中一处。至于“要不要动系统参数”这个问题本文不给答案。它属于运维调优前提是你已经排除了前三处而且清楚代价。在没排除之前动参数等于把症状盖住。完整版环境对照表第七章这五步在两套系统上分别该敲哪条命令对照表里列了同一件事的两种写法省得你在 macOS 和 Linux 之间来回试。放在资料包里扫码即可获取附表 A本文引用事实与官方出处对照表编号事实要点出处本文位置TW1主动关闭的连接必须在 TIME-WAIT 停留 2×MSLMUST-13TIME-WAIT 下可凭序列号条件接受新 SYNrfc-editor.org《RFC 9293》第 3.5 节1.2、3.2TW2TIME-WAIT 状态的定义等够时间以确认对端收到终止确认并避免新连接受旧连接延迟报文影响rfc-editor.org《RFC 9293》第 3.4.2 节2.3、3.1TW3本规范取 MSL 为 2 分钟属工程选择可依经验更改rfc-editor.org《RFC 9293》第 3.4.2 节4.2TW4报文占用的序列号在 MSL 秒内视为占用中rfc-editor.org《RFC 9293》第 3.4.3 节2.3、4.3TW5SO_REUSEADDR 放宽地址校验但有活跃监听套接字时仍不可绑SO_REUSEPORT 允许多个套接字绑同一地址需各自设置且有效 UID 相同man7.org《socket(7)》Linux man-pages6.1、6.2、6.3TW6本机临时端口区间 49152–65535TIME-WAIT 记录锁住的是本地临时端口本机实测macOS2026-10-205.3TW7服务端主动关闭时请求方一侧是否进入 TIME-WAIT、各发行版默认取值未逐字核到待验证5.2附表 B术语速查表术语一句话解释主动关闭方先发出连接结束请求的那一端只有它需要停在 TIME-WAIT被动关闭方收到结束请求后才关闭自己那一端的一方TIME-WAIT主动关闭方在连接结束后的一段等待期用于兜住丢失的确认与延迟的旧报文MSL报文在网络中存活的最长时间规范取 2 分钟是假设值不是测量值2×MSL规范算出的 TIME-WAIT 停留时长按 MSL2 分钟即 4 分钟四元组源地址、源端口、目的地址、目的端口唯一确定一条连接临时端口系统分配给对外发起连接的一侧使用的端口区间本机为 49152–65535监听套接字处于监听状态、等待新连接进来的套接字与已建立的连接不是一回事SO_REUSEADDR放宽绑定时的地址校验但有活跃监听时仍然绑不上SO_REUSEPORT允许多个套接字绑定同一地址需各方都设置且有效 UID 相同序列号空间每个报文占用的编号区间在一段时间内视为仍被占用是 TIME-WAIT 存在的底层理由写在最后这篇用到的资料写这篇文章时我为了确认“被占的到底是哪个端口”在自己机器上把连接表和临时端口区间都拉出来对了一遍才发现很多人嘴里的“端口没放出来”其实和服务端口一点关系都没有。顺手也整理了几份配套的东西靶场环境对照表DVWA、upload-labs 在 Windows / macOS / Linux 三平台的可行性与推荐路径Web 安全学习路线图从基础打牢到安全管理四个阶段各学什么常用靶场清单每个靶场练什么、适合哪个阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「靶场」优先通过。拿到之后建议先看环境对照表那一份把同一件事在两套系统上的写法对齐下次再遇到“端口没放出来”先看四元组别急着动系统参数。
阅读完成 · 觉得有帮助?