1. 为什么打流时要把源端口“钉死”
做网络测试的兄弟应该都有过这种经历:在测试环境里用压测工具打流,明明只是想验证防火墙策略,结果发现源端口一直在变,抓包抓得到一堆随机端口,规则匹配不上,NAT 会话也老串。这时候你就会意识到,打流虽然看起来是把包发出去,但能不能“指定源端口”这件事,直接决定了你测试结论可不可信。这篇文章就把 Linux 环境下几种常见的指定源端口打流方法一次讲透:hping3、netcat、socat、iperf3、Scapy,什么场景用什么,怎么验证,哪些坑千万别踩。
先说打流。所谓打流,就是主动向目标网络设备或目标主机发送特定速率、特定协议的数据包,用来评估带宽、延迟、丢包率、防火墙规则是否生效、负载均衡是否按预期调度。常用工具包括 iperf3、hping3、Scapy、socat 这些。而一次普通的 TCP 或 UDP 通信,报文头里有两个端口:源端口和目的端口。目的端口是访问服务的“入口”,比如 80、443、53;源端口则是发起方这边用来区分会话的编号,正常情况由操作系统从临时端口段里挑。内核给自动连接分配的临时源端口范围,在 Linux 里通常写在/proc/sys/net/ipv4/ip_local_port_range里,常见取值是 32768 到 60999。简单理解,如果你不干预,连续发 100 个连接,源端口大概率是分散在这一大段里的。
绝大多数场景下,内核自动选源端口是完全没问题的,随机端口反而能减少冲突。但下面这些场景,源端口随机就会让测试变得很难受:
- 防火墙或安全组策略验证:策略精确到“仅允许来自源端口 8000-8050 的流量通过”时,你打流的源端口却是内核随机挑的,流量可能因为不在允许段内被直接丢掉,测试结论根本不能参考。
- NAT 行为复现:有些 NAT 设备做端口映射时,对外端口和内部源端口一一对应。源端口不稳定,映射关系一乱,想要定位问题就特别费劲。
- 负载均衡一致性验证:无状态负载均衡常按四元组做哈希,源端口一变,哈希结果就变,连接会飘到不同后端,你没法判断集群调度到底对不对。
- 模拟老业务连接:部分历史遗留应用会把本机源端口写死在代码里,要复现它们的连接行为,必须用相同源端口打流才能贴近真实情况。
- 抓包过滤简单化:固定源端口之后,tcpdump 过滤条件可以写得很清爽,比如
tcp src port 40000,抓到的包一眼就能认出是本次测试流量,不会混入其他噪声。
需要先说明一点:源端口只存在于 TCP 和 UDP 这类传输层协议里,ICMP、IGMP 这类协议没有端口概念。所以“指定源端口打流”主要针对 TCP 和 UDP 流量,后面的方案也全部围绕这两个协议展开。
2. 工具能力盘点:先看清“谁能改源端口”
2.1 常见打流工具的源端口支持情况
不是所有流量工具都能改源端口。很多工具在设计上就是“由内核自动分配端口”,比如 curl、wget,它们没有暴露设置源端口的选项。下面这张表是我常用的几类工具的能力边界,可以帮你快速判断选型方向。
| 工具 | TCP 指定源端口 | UDP 指定源端口 | 主要优势 | 主要限制 |
|---|---|---|---|---|
| hping3 | 支持(-s) | 支持(-s) | 命令行灵活,协议控制能力强,单机即可发包 | 收发性能一般,不适合万兆大流量压测 |
| netcat / ncat | 支持(-p 或 --source-port) | 支持(-u 时同样可用) | 随处可得,适合快速建连和连通性验证 | 流控能力弱,不适合持续高压打流 |
| socat | 支持(bind=) | 支持(bind=) | 能与文件、管道、外部进程组合,玩法多 | 配置略绕,性能中规中矩 |
| iperf3 | 部分版本支持(--cport) | 部分版本支持(--cport) | 自带带宽和丢包统计,适合性能基线 | 很多发行版默认版本没编译该选项 |
| Scapy | 支持(sport=) | 支持(sport=) | 报文里任何字段都能改,协议级自由操作 | 发包性能弱,需要 Python 环境和 root 权限 |
| Python socket | 支持(bind) | 支持(bind) | 可编程,适合自动化回归和并发场景 | 需要写代码,高性能并发需要额外处理 |
2.2 按需选型:要带宽还是要精确
选工具不能只看“能不能指定源端口”,还要看你的测试目标是什么。如果只是验证防火墙的一条规则,那用 hping3 发几个 SYN 包就够了,没必要上 iperf3,因为它要搭客户端和服务器端,成本高。如果是打真实业务流量的带宽基线,想固定源端口的同时又看吞吐和丢包率,那 iperf3 更合适,前提是版本支持 --cport。如果是要模拟几十路不同源端口的并发连接,Python 脚本最省事,循环 bind 一次就行。如果是构造一些畸形报文、特定 TCP 标志的流量,Scapy 是唯一能让你“为所欲为”的选择。
一句话总结:快速验证选 hping3,压力基线选 iperf3,连接级验证选 nc/socat,自动化或特殊报文选 Scapy/Python。下面逐个展开。
3. 方案一:hping3,命令行高压打流首选
3.1 安装与参数速记
hping3 是一个非常经典的命令行发包工具,它支持 TCP、UDP、ICMP 和 RAW IP 模式,也是我平时指定源端口最常用的工具。安装很简单,Debian/Ubuntu 直接apt install hping3,CentOS/RHEL 用dnf install hping3,如果没有包,去官方仓库拉源码编译也就几分钟的事。
用 hping3 之前,建议先把几个核心参数记牢,不然每次都翻帮助页很影响效率:
-S:TCP SYN 模式,这是最常用的一种 TCP 打流方式。-2:切到 UDP 模式,注意是小写数字 2。-p:目的端口,也就是目标服务的端口。-s:源端口,这是本文的核心参数。-a:伪造源 IP 地址,需要 root 权限。-I:指定从哪块网卡发包,多网卡机器上必须加。-c:发送包的数量。-i:发包间隔,比如u100表示间隔 100 微秒。--flood:不考虑响应速度,能发多快发多快,慎用。
3.2 TCP 打流指定源端口
假设你在一台测试机上,目标机器是 192.168.1.10,需要从源端口 40000 发 TCP SYN 包访问目标 80 端口,命令如下:
hping3 -S -p 80 -s 40000 192.168.1.10 -c 100这条命令会从本机发出 100 个 TCP SYN 包,每个包的源端口都是 40000,目的端口是 80。目标如果收到并回复 SYN-ACK,源端口也必然是 40000。这里最容易犯的错误就是把-p当成源端口,实际上 hping3 里-p是目标端口,源端口必须用-s。我见过不少同事在这上面栽过跟头,抓包一看源端口是随机数,还以为工具坏了。
如果本机有多块网卡,想让流量从特定网卡出去,加-I参数:
hping3 -I eth0 -S -p 80 -s 40000 192.168.1.10 -c 100还可以顺带指定源 IP,比如模拟另一个内网地址发起的连接:
hping3 -a 192.168.1.20 -S -p 80 -s 40000 192.168.1.10 -c 100注意-a只是把报文里的源 IP 改了,本机网卡和路由行为不会变,这种伪造包在测试时一定要确保网络架构允许,避免干扰到其他正常业务。
3.3 UDP 打流指定源端口
UDP 打流同样简单,加一个-2参数进入 UDP 模式。例如从源端口 5353 发 UDP 包到目标 53 端口,包大小 512 字节:
hping3 -2 -p 53 -s 5353 192.168.1.10 -c 100 -d 512-d指定数据负载大小。UDP 打流时目标端口通常是一个真实监听的 UDP 服务,或是一个能用nc -u -l临时拉起来的监听,否则你发了 100 个包,可能一个回复都收不到。这并不代表发包失败,只能说明服务端没回包。想确认是否收到,可以在对端用tcpdump -ni eth0 udp port 53抓包验证。
还有一种常见做法是配合-i u100控制发包速率:
hping3 -2 -p 53 -s 5353 192.168.1.10 -c 1000 -i u100u100表示每 100 微秒发一个包,算下来大约是每秒 1 万个包。这个速率在局域网里基本不会把链路打满,但足以用来验证 UDP 通断和防火墙行为。
3.4 按端口段批量打流
很多时候不是固定某一个源端口,而是要“以 40000 到 40010 这 11 个源端口依次打流”。这种情况最直接的办法是用 shell 循环:
for p in $(seq 40000 40010); do hping3 -S -p 80 -s $p 192.168.1.10 -c 3 -i u200 done这个写法有几个好处。第一,每个源端口只发 3 个包,循环切换很快,不会长时间占住同一端口导致后续端口被 TIME_WAIT 占用。第二,配合-i u200控制包速,目标防火墙如果配置了连接速率限制,也不容易被误伤。第三,shell 循环本身逻辑透明,出了问题时你很容易判断是哪个源端口引起的。
实际跑这种批量打流时,我建议同时打开一个 tcpdump 在后台抓包,比如tcpdump -ni any tcp port 80 -w batch.pcap,打完后用 Wireshark 按tcp.srcport排序,一眼就能确认每一个源端口是否都发到了目标端。
4. 方案二:netcat 系与 socat,适合连接级验证
4.1 先弄清 nc 的 -p 参数带不带坑
netcat 常被称为“网络瑞士军刀”,在命令行下非常方便,但它在不同发行版上的实现实在太多,这是最容易踩坑的地方。先说结论:在大多数实现里,主动发起连接时,-p后面跟的就是本地源端口,目的端口是最后的位置参数。
例如传统 netcat 发起 TCP 连接,指定源端口 40000,访问目标 80 端口:
echo test | nc -p 40000 192.168.1.10 80如果是 UDP 模式:
echo test | nc -u -p 5353 192.168.1.10 53但你要注意,有些系统的 nc 其实是 nmap 的 ncat,它从命令行上还提供--source-port这种更明确的写法:
ncat --source-port 40000 192.168.1.10 80如果系统上的 nc 恰好是某个精简版本,-p的行为可能会让你困惑。所以进入一个新环境时,先敲nc -h看一眼帮助,确认里面写到的是-p port还是--source-port port,这个几十秒的检查能帮你省下一堆排查时间。
还有一点必须提醒:不要在打流的时候试图用同一个 nc 命令既指定源端口又监听端口。监听场景里各家实现差异更大,传统 nc 是nc -l -p 8080,OpenBSD nc 是nc -l 8080,ncat 两者都能收。我们做源端口打流时基本都是主动连接模式,指定源端口就用-p或--source-port,目的端口用位置参数,这样最稳。
4.2 socat 绑定源端口
socat 比 netcat 功能更强,它擅长把两个数据通道拼接在一起,比如“文件到 TCP”“TCP 到 UDP”“Unix Socket 到 TCP”。指定源端口时,socat 用的是bind=选项,格式是bind=IP:PORT。
比如要把本机 192.168.1.20 上的 40000 端口作为源端口,向 192.168.1.10 的 80 端口发一段数据:
echo hello | socat - TCP4:192.168.1.10:80,bind=192.168.1.20:40000UDP 也是一样的思路:
echo hello | socat - UDP4:192.168.1.10:53,bind=192.168.1.20:5353这里的-表示标准输入输出,因为我们需要把数据喂进去。socat 的bind=可以只写 IP 不写端口,也可以只写端口不写 IP,比如bind=:40000表示只绑定源端口 40000,源 IP 由路由决定。实际测试里,我更多是 IP 和端口一起写死,这样抓包时四元组完全可控。
socat 还有一个比 nc 强的地方是它可以配合管道做持续数据流。比如要持续发送 100MB 数据给某个 UDP 端口:
dd if=/dev/zero bs=1M count=100 | socat - UDP4:192.168.1.10:9000,bind=192.168.1.20:40000不过 socat 本质上还是面向连接和数据的工具,不是专门的高性能打流器。真要大流量压测,还得回到 iperf3 或专用发包工具上。
4.3 iperf3 --cport 也能做到
很多人不知道,iperf3 也有固定客户端源端口的选项,叫--cport。它的语法是在客户端侧指定:
iperf3 -c 192.168.1.10 -u -b 100M -t 10 -p 5201 --cport 40000这条命令的含义是客户端从源端口 40000 发出 UDP 流量,带宽目标是 100Mbps,持续 10 秒,连接目标即 iperf 服务端的 5201 端口。服务端需要提前启动:
iperf3 -s这个方案最大的价值在于它不仅能固定源端口,还能同时输出带宽、丢包率、抖动等指标。做网络质量基线测试时,固定源端口的意义是让四元组稳定,避免因为源端口变化导致路径或负载均衡对象漂移。
不过 --cport 不是所有版本都支持。我就遇到过一台发行版自带的 iperf3,编参数时怎么都不认 --cport,后来换用源码编译的新版本就正常了。遇到这种情况先跑一下iperf3 -h | grep cport,如果没有输出,就用 hping3 或 Python 方案顶上。
5. 方案三:Scapy 与 Python socket,自由协议级控制
5.1 Scapy:包里的任何字段都能改
Scapy 是基于 Python 的报文构造工具,它最大的优势是“什么都拦不住你”:TCP 头里的 flags、窗口、序号,IP 头里的 TTL、TOS,甚至下面的以太网源目 MAC,全都可以手动指定。指定源端口只是它能力清单里最简单的一条。
安装 Scapy 很直接:
pip3 install scapy然后构造一个 TCP SYN 包,源端口 40000,目的端口 80:
from scapy.all import * send(IP(src="192.168.1.20", dst="192.168.1.10") / TCP(sport=40000, dport=80, flags="S"), verbose=False)注意send()是只发包不收包,如果你希望等一个回复,用sr1():
ans = sr1(IP(src="192.168.1.20", dst="192.168.1.10") / TCP(sport=40000, dport=80, flags="S"), timeout=3, verbose=False) if ans: ans.show()UDP 打流也是一样,比如从源端口 5353 发一个 DNS 查询到目标 53 端口:
from scapy.all import * send(IP(src="192.168.1.20", dst="192.168.1.10") / UDP(sport=5353, dport=53) / DNS(rd=1, qd=DNSQR(qname="test.example.com")), verbose=False)用 Scapy 的好处是写测试场景极其直观,比如模拟一个带 MSS 值的 SYN,或者打了 TCP 时间戳的包,代码里改一行参数就能实现。代价是性能弱,它走的是普通 socket 发包路径,单核每秒也就几千到几万个包,完全不指望它打满万兆。所以 Scapy 适合做小流量的协议级测试,不适合压测。
一个容易忽略的点是:Scapy 发包需要 root 权限,因为它要创建 raw socket 来构造报文。如果当前用户没有 root,敲完代码会提示权限不足。另外,如果你只想在局域网内发 L2 数据帧,可以不构造 IP 层,直接用Ether(dst="ff:ff:ff:ff:ff:ff")这种写法。
5.2 Python 标准库:bind 之后再连接
不想引入 Scapy 依赖时,Python 标准库里的 socket 也能实现指定源端口打流。核心思想是:先bind()到本机某个端口,再connect()去发起连接。这个 bind 的端口就是源端口。
TCP 示例:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("0.0.0.0", 40000)) s.connect(("192.168.1.10", 80)) s.sendall(b"GET / HTTP/1.0\r\n\r\n") s.close()UDP 示例更简单,因为 UDP 是面向无连接的,bind 后直接 sendto:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("0.0.0.0", 5353)) s.sendto(b"\x00" * 64, ("192.168.1.10", 53)) s.close()这里有三个细节值得注意。第一,bind 里的 IP 可以写"0.0.0.0",表示由路由决定从哪块网卡出去,也可以像"192.168.1.20"这样指定明确的本机 IP,效果等同于 hping3 的-I。第二,TCP 连接建立后如果直接 close,端口会进入 TIME_WAIT 状态,短时间再次绑定同一源端口会报Address already in use,所以我加了SO_REUSEADDR来缓解。第三,源端口理论上可以是 0 到 65535 的任意数字,但是 0 到 1023 通常需要 root 权限,而且临时端口范围只影响内核自动分配,你显式 bind 一个范围内的端口并不受 ip_local_port_range 限制。
5.3 多源端口并发连接的自动化脚本
如果只是想测防火墙对一段源端口的放行策略,用 Python 循环批量建连再合适不过。下面的脚本会从 40000 到 40099 依次发起 TCP 连接,保持一段时间后关闭:
import socket import time import concurrent.futures DST = ("192.168.1.10", 80) BASE_PORT = 40000 COUNT = 100 def open_connection(sport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("0.0.0.0", sport)) s.connect(DST) return s with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: conns = list(executor.map(open_connection, range(BASE_PORT, BASE_PORT + COUNT))) time.sleep(30) for c in conns: c.close()这个脚本的价值在于模拟了大量“固定源端口的长连接”。某些负载均衡或防火墙会按源端口做会话保持,你用这种方式能快速验证:当 100 个不同源端口同时打过来时,目标侧是否会把流量分摊到预期后端,以及断开后重新建立是否仍能保持原来的调度关系。把这个脚本稍加改造,把 DST 换成服务端地址,就能做成一个每晚运行一次的自动化回归用例,报告里记录每个源端口的建连成功率,省去很多人工操作。
并发数要控制好。上面的脚本只开了 20 个线程,即使目标防火墙有较严格的每秒连接数限制,也不容易触发限速。如果目标端是 Linux,默认的 SYN 队列和全连接队列有上限,同时打几百上千个端口很容易出现 SYN 丢包,测试结果会被误判成“防火墙拦截”,到时候又要排查半天。建议先小规模验证,再逐步放大。
6. 如何确认源端口真的生效
6.1 本机抓包看四元组
指定源端口之后,第一件要做的事不是看服务器结果,而是在本机抓包确认数据包确实以目标源端口发出了。抓包命令很简单,比如确认 TCP 40000 端口的包:
tcpdump -ni any -c 10 "tcp and src port 40000"确认 UDP 5353 端口:
tcpdump -ni any -c 10 "udp and src port 5353"-n表示不解析域名和端口名,-i any表示监听所有网卡。如果抓不到你指定的源端口,说明参数没写对、网卡选错或流量走了别的地方。很多时候用户会用-i eth0抓包,但流量的确是从 eth1 出去的,就会产生“明明指定了端口却抓不到”的错觉。
如果是批量打流场景,直接把抓包存成 pcap 文件,用 Wireshark 打开后按tcp.srcport或udp.srcport排序,所有源端口的分布情况一目了然。我习惯先在本机抓一次包确认,再到对端抓一次包确认,中间只要有一次出现“对端看到的源端口变了”,那就是 NAT、负载均衡或安全设备在改写端口,这时候要排查的就不是工具本身了。
6.2 服务端观察连接状态
服务端也可以直接观察连接四元组。假设目标服务监听在 80 端口,你从 40000 端口发起了连接,那么在服务端执行:
ss -tn | grep 40000正常情况下你会看到类似这样的输出:
ESTAB 0 0 192.168.1.10:80 192.168.1.20:40000这个结果说明服务端已经收到了来自源端口 40000 的数据包,并且成功建立了 TCP 连接。如果服务端收到的是别的源端口,或连接根本没建立,那就要回到本机抓包那一环继续排查。
UDP 场景下服务端的状态表不直观,因为 UDP 是无状态的。更可靠的方式是在服务端直接抓包:
tcpdump -ni eth0 -c 10 "udp and port 53"看到192.168.1.20.5353 > 192.168.1.10.53这样的记录,就说明源端口已经生效了。
6.3 常见误区与避坑清单
下面这些坑基本都是我在实际测试中反复遇到的,整理成速查表,可以当成一个检查清单来用。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 提示 Address already in use | 源端口被其他进程占用,或 TCP 端口处于 TIME_WAIT | 先ss -lntp | grep 端口查占用;等两个 MSL 周期,或换一个源端口段 |
| 抓包看到源端口不是指定的 | -s参数写错、绑定 IP 不对、中间有 NAT 改写 | 确认命令参数;确认源 IP 是否匹配网卡;在更靠近本机的交换机端口上抓包 |
| 指定的源端口发不出来 | 端口号超出 0-65535,或使用 0-1023 无 root 权限 | 换用高位端口,或用 sudo 执行 |
| TCP 打流目标没反应 | 发送的是 SYN 包,目标防火墙直接丢弃,或目标端口没监听 | 先用 nc 手动测试目标端口;确认防火墙放行策略 |
| UDP 打流目标不回包 | UDP 无连接状态,目标服务可能没有回包逻辑 | 在对端抓包确认收到即可,不一定需要回包 |
| 用网卡 A 的 IP 指定的源地址,流量却从网卡 B 出去 | 路由策略优先于 bind 的源 IP | 用ip rule和ip route查看策略路由;或者直接绑定网卡发包 |
| hping3 打流 CPU 100%,速率上不去 | hping3 单线程,单包处理开销高 | 接受其性能边界;大流量场景换 iperf3 或多开进程分源端口段发送 |
| iperf3 提示 unrecognized option '--cport' | 发行版自带的 iperf3 编译时没有开这个选项 | 升级到新版本,或改用 hping3/Scapy 方案 |
还有一个经验层面的建议:固定源端口进行持续打流时,尽量选择不落在系统临时端口范围内的端口段,比如 41000 到 41999 这类相对“冷门”的区间。因为如果你的源端口恰好落在 32768 到 60999 的自动分配范围里,其他正常应用先一步把端口占用了,你的测试脚本就会时不时报 bind 失败。选一个冷门段位,既降低了冲突概率,也方便后续出问题时用ss -lntp | grep 41快速定位是谁占用了端口。
按我自己的习惯,现场快速排障先用 hping3 一条命令搞定,自动化回归用 Python 脚本,要测带宽基线就确认 iperf3 版本后加--cport。所有操作都建议限定在自有测试环境或明确授权的网络里进行,毕竟指定源端口打流会直接影响目标设备的当前连接状态,在生产环境里误操作的风险远大于测试带来的收益。