简介:一款基于Java开发的网络封包全套工具,面向网络运维、Java开发者和安全测试人员,用于捕获、发送、拦截及分析网络数据包。工具围绕协议解析、自定义发包、流量过滤和日志记录展开,支持TCP、UDP、HTTP等常见协议,可辅助排查接口故障、验证服务端逻辑或进行基础安全测试。压缩包采用RAR格式,包体大小约2.47MB,文件总数记录为0,具体文件类型未列出,内容需下载解压后查看。目前已有341人学习浏览,适合初中级网络技术人员快速上手封包操作。通过该资源可获取完整的Java封包程序框架、功能模块说明及抓包分析思路,既能复用核心代码开展二次开发,也能直接用于日常调试、数据包重放与网络异常定位,有效节省从零搭建抓包环境的时间。此外,工具具备跨平台特性,可在Windows、Linux与macOS环境下运行,便于不同系统使用者直接部署。
1. 抓到的包不是字节,是协议栈的断层扫描
去年排查一个订单系统的偶发超时,Wireshark 停在 TCP 重传上就没了下文,真正的问题出在 Java 应用把几个业务报文塞进了同一个 TCP 段里,Wireshark 按流重组后根本看不出原始边界。后来翻出这套名为“血杀封包全套工具”的 Java 封包套件,发现它把捕获、解析、发送、拦截四件事统一在一个命令入口上,才意识到日常用的图形化抓包工具只是消费端,真正干活的是协议栈底层的原始字节。如果你经常要验证网络服务、模拟客户端行为,或者写自动化网络测试,基于 Java 的封包工具比 Wireshark 更容易嵌入到测试代码和 CI 流程里,而且跨平台不用换三套驱动。
2. Java 封包捕获:pcap4j 的设备轮询与离线回放
封包捕获在 Java 里不是Socket接口能直接触达的。Java 标准库只提供流式通信抽象,网卡处于混杂模式后收到的数据包不会经过 TCP/IP 栈,自然也不会进入ServerSocket。所以这一层的核心是找一个能穿透 JVM 的桥:pcap4j。
2.1 捕获原理:libpcap 与 JNI
libpcap 是 Linux、Windows、macOS 上事实标准的抓包库。它把网卡的全部链路层帧复制一份交给用户态程序,应用层代码可以在不干扰协议栈的情况下读到原始以太网帧。pcap4j 通过 JNI 调用 libpcap,但 JNI 不是重点,重点是你必须先把本机的抓包依赖装好:Linux 上安装libpcap-dev,Windows 上装 Npcap 或老版 WinPcap。这也是很多人第一次跑 pcap4j 就报UnsatisfiedLinkError的根源。
先确认你的 java 环境变量配置没问题,java -version能正常输出,然后再看 pcap4j 的核心依赖:
<dependency> <groupId>org.pcap4j</groupId> <artifactId>pcap4j-core</artifactId> <version>1.8.2</version> </dependency> <dependency> <groupId>org.pcap4j</groupId> <artifactId>pcap4j-packetfactory-static</artifactId> <version>1.8.2</version> </dependency>带上pcap4j-packetfactory-static是为了让 pcap4j 能够自动把原始字节解析成IpV4Packet、TcpPacket等对象。如果你只想拿byte[]自己解析,可以不加这个依赖。依赖和原生库都就位后,用一个典型的实时抓包流程来看整个链路。
2.2 实时抓包:用 PcapHandle 打开网卡
import org.pcap4j.core.*; import org.pcap4j.packet.Packet; public class CaptureDemo { public static void main(String[] args) throws Exception { // 枚举所有网卡,找到名为 eth0 的那张 PcapNetworkInterface nif = Pcaps.getDevByName("eth0"); if (nif == null) { throw new IllegalStateException("eth0 not found, check your device name"); } // snaplen=65536, promiscuous=混杂模式, timeout=10ms PcapHandle handle = nif.openLive(65536, PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, 10); // 最多抓 20 个包后停止 PacketListener listener = packet -> { System.out.println("captured: " + packet.length() + " bytes, at " + packet.getTimestamp()); }; handle.loop(20, listener); handle.close(); } }参数需要说清楚:snaplen告诉内核最多复制每个包的多少字节,65536 能覆盖最大以太网帧;promiscuous决定网卡是否接收目的地不是本机的包,抓本机进出流量时不开也能拿到,想观察局域网里的广播包才需要开;timeout是抓包超时阈值,这里写 10ms 表示即使没有达到数量上限,每 10ms 回调一次 listener,避免界面假死。handle.loop(20, listener)是阻塞式调用,适合 demo;实际服务里应该把PcapHandle放进单独的线程,并考虑用breakLoop()优雅停止。
2.3 离线导入:从 pcap 文件读封包
很多场景没有权限在目标机器上实时抓包,别人会导出一个.pcap文件给你。pcap4j 同样能打开离线文件,而且 API 和实时抓包几乎一致:
PcapHandle offHandle = Pcaps.openOffline("/tmp/order-timeout.pcap"); Packet packet; while ((packet = offHandle.getNextPacket()) != null) { if (packet.contains(TcpPacket.class)) { TcpPacket tcp = packet.get(TcpPacket.class); System.out.printf("src port: %d, dst port: %d, seq: %d%n", tcp.getHeader().getSrcPort().valueAsInt(), tcp.getHeader().getDstPort().valueAsInt(), tcp.getHeader().getSequenceNumber()); } } offHandle.close();注意openOffline不需要管理员权限,因为它只是读取文件,不碰真实网卡。这里我用packet.get(TcpPacket.class)做协议类型判断和字段提取,pcap4j 会自己处理以太网帧和 IPv4 头,你不需要关心偏移量。离线回放的价值在于可复现,特别是在定位时序问题时,同一份 pcap 可以反复跑不同解析逻辑,不会因为网络环境变化干扰结论。
2.4 常用 BPF 过滤与参数坑
在openLive之后还可以给handle设置内核级过滤,减少用户态处理压力:
| 过滤需求 | BPF 表达式 |
|---|---|
| 只抓 80 端口 TCP 流量 | tcp port 80 |
| 抓本机到指定 IP 的流量 | host 10.12.23.1 |
| 抓 443 端口的 TLS 握手包 | tcp port 443 and tcp[tcpflags] & tcp-syn != 0 |
| 抓所有 ARP 包 | arp |
设置方法极简单:handle.setFilter("tcp port 80", BpfProgram.BpfCompileMode.OPTIMIZE);。这里有个最容易踩的坑:setFilter必须在openLive之后,且一旦设置了过滤,离线文件里的包也会被过滤,所以同一条逻辑要复用时,最好把过滤表达式作为参数传进去,而不是在代码里硬编码。
还有一个高频坑是 Windows 下 pcap4j 会找不到 Npcap 的 DLL。你的Path环境变量里必须有 Npcap 的安装目录,否则 JVM 启动时加载不到原生库。检查时不要只看java -version,还要确认System.getProperty("java.library.path")是否能定位到npcap目录。这本质上和 java 环境变量配置是一类问题,运行环境没准备好,再好的抓包逻辑也起不来。
3. 封包分析:从字节数组还原 TCP/IP 字段
抓到原始字节后,真正耗时的功夫是从字节序里恢复协议语义。很多开发者在面试八股文里背过 TCP 头部结构,但真到需要从byte[]里抽出 sequence number 时,反而被大小端问题绊住。
3.1 数据包模型的抽象设计
我先定义一个最简的PacketFrame接口,保证后续分析代码不依赖具体协议库:
public interface PacketFrame { int protocol(); int srcPort(); int dstPort(); byte[] payload(); }这个接口只有四个方法,但已经够覆盖 TCP/UDP 常见场景。protocol返回 IP 协议号,TCP 是 6,UDP 是 17;srcPort/dstPort在传输层头部;payload是去掉 IP 头和传输层头之后真正的业务数据。设计时不要让具体协议解析细节污染上层业务,这样后续加入 HTTP 解析、DNS 解析时,调用方代码不用改。这也是 Java 基础里接口隔离原则的实际应用。
3.2 基于 ByteBuffer 解析 IPv4 头
拿到byte[] raw后,第一步是定位 IP 头的起点。如果在以太网帧上,通常前 14 字节是 MAC 地址和类型字段,从第 14 字节开始是 IP 头。下面是一段不依赖 pcap4j 对象模型的解析示例:
public static int parseIpv4Header(byte[] raw, int offset) { // 以太网帧头固定 14,IP 头从 offset=14 开始 ByteBuffer buf = ByteBuffer.wrap(raw, offset, raw.length - offset) .order(ByteOrder.BIG_ENDIAN); int versionIhl = buf.get() & 0xff; int version = (versionIhl >> 4) & 0xf; int ihl = (versionIhl & 0xf) * 4; if (version != 4) { System.out.println("not an IPv4 packet"); return -1; } int totalLength = buf.getShort() & 0xffff; int identification = buf.getShort() & 0xffff; int flagsAndFragment = buf.getShort() & 0xffff; int ttl = buf.get() & 0xff; int protocol = buf.get() & 0xff; int headerChecksum = buf.getShort() & 0xffff; int srcIp = buf.getInt(); int dstIp = buf.getInt(); System.out.printf("ipv4 ver=%d ihl=%d totalLen=%d proto=%d src=%s dst=%s%n", version, ihl, totalLength, protocol, intToIp(srcIp), intToIp(dstIp)); return offset + ihl; } private static String intToIp(int ip) { return String.format("%d.%d.%d.%d", (ip >> 24) & 0xff, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff); }这个解析逻辑的关键在于ByteOrder.BIG_ENDIAN。网络字节序本身就是大端,Java 的ByteBuffer默认也是大端,但如果你用它默认的allocate,还是要显式声明order,防止后面改成小端模式时忘记调整。versionIhl一个字节里同时放了版本号和头部长度,高位 4 bit 是版本,低位 4 bit 是以 4 字节为单位的头部长度。ihl乘以 4 才得到真正的头字节数,常见值是 20。headerChecksum字段需要单独做校验和验证,属于 Java 网络编程里容易被忽略的点。
3.3 校验和验证与分片重组
IPv4 头部校验和是端到端可靠性的第一道屏障,很多抓包文件里有坏包,不验证直接用会得到错误结论。校验和算法是把头部按 16 bit 分组累加,再把进位回卷,最后取反:
public static boolean verifyIpChecksum(byte[] header) { byte[] copy = Arrays.copyOf(header, header.length); copy[10] = 0; // 清零 checksum 字段 copy[11] = 0; int sum = 0; for (int i = 0; i < copy.length; i += 2) { sum += ((copy[i] & 0xff) << 8) | (copy[i + 1] & 0xff); } while ((sum >> 16) != 0) { sum = (sum & 0xffff) + (sum >> 16); } return (sum & 0xffff) == 0xffff; }如果返回 false,说明这个包在传输过程中头部某一位发生了翻转。实际分析中不要看到坏校验和就跳过,常见原因是网卡 TCP 校验和卸载(TSO)导致补丁包,IP 头校验和一般不会错;如果大量出现 IP 校验和错误,优先怀疑抓包工具本身或者驱动,而不是对端主机。
分片重组是另一个大坑。IPv4 的分片偏移字段在flagsAndFragment的低 13 bit,单位是 8 字节;当一个包被分片后,后续分片的 IP 头里没有传输层端口,只有第一个分片才有。所以解析传输层端口前必须检查分片偏移是否为 0,否则读到的端口字段是垃圾数据。常见做法是维护一个以identification + srcIp + dstIp为 key 的重组表,等所有分片到期后再拼装。
3.4 HTTP/DNS 层解析的思路
传输层之外,业务最关心的是 HTTP 和 DNS。拿到 TCP payload 后,HTTP 解析相对简单,只需要按\r\n\r\n切分头部,再根据Content-Length读 body。DNS 则复杂得多,它的头部固定 12 字节,后面的查询记录用长度前缀字符串,且域名会做压缩指针跳转。我的建议是不要自己造轮子,直接用 dnsjava 或 Netty 的 codec 去处理,但前提是你已经把 IP 和传输层头剥干净。很多时候封包工具里“抓到了但看不到协议”的原因,就是没有把上层 payload 完整提取出来,尤其遇到 TCP 分段时,payload 可能跨多个包,必须做流重组才能还原 HTTP 请求。流重组是个独立模块,这里先不展开,后文发送封包时再回来看它的验证作用。
4. 发送封包:构造自定义报文并注入测试环境
封包工具的另一半能力是“发”,不仅是Socket.write()那种从应用层发标准字节流,而是能在 IP 层构造任意 flag、任意端口、任意 payload 的原始包。这对测试服务器异常处理、模拟 DDoS 流量里的攻击特征、验证防火墙规则都非常有用。
4.1 两种发包路径:原始 Socket vs pcap 发包
Java 里发送自定义封包有两条路:
| 方法 | 优势 | 限制 |
|---|---|---|
DatagramSocket/Socket | 代码简单,不依赖原生库 | 只能发 TCP/UDP 标准连接,不能改 IP 头 TTL、TOS 等字段 |
pcap4j 的PcapHandle.sendPacket | 可以构造完整的以太网帧,源 MAC、源 IP 都能伪造 | 需要管理员权限,发送的包不带操作系统协议栈状态 |
多数封包工具实现的是 pcap 发包。因为只有这条路能让你发一个“本机根本不存在的 IP 地址”的封包,用于测试路由器转发逻辑。下面用 pcap4j 构造一个 UDP 包。
4.2 用 pcap4j 构造并发送 UDP 封包
import org.pcap4j.packet.*; import org.pcap4j.packet.namednumber.*; import org.pcap4j.util.MacAddress; public class SendUdpPacket { public static void main(String[] args) throws Exception { byte[] payload = "hk-test".getBytes("UTF-8"); Inet4Address srcIp = (Inet4Address) InetAddress.getByName("192.168.1.10"); Inet4Address dstIp = (Inet4Address) InetAddress.getByName("8.8.8.8"); UdpPacket udp = new UdpPacket.Builder() .srcAddr(srcIp) .dstAddr(dstIp) .srcPort((short) 12345) .dstPort((short) 53) .payloadBuilder(new UnknownPacket.Builder().rawData(payload)) .correctChecksumAtBuild(true) .build(); IpV4Packet ipv4 = new IpV4Packet.Builder() .version(IpVersion.IPV4) .tos((byte) 0) .ttl((byte) 64) .protocol(IpNumber.UDP) .srcAddr(srcIp) .dstAddr(dstIp) .payloadBuilder(udp) .correctChecksumAtBuild(true) .build(); EthernetPacket ether = new EthernetPacket.Builder() .srcAddr(MacAddress.getByName("AA:BB:CC:DD:EE:FF")) .dstAddr(MacAddress.getByName("00:11:22:33:44:55")) .type(ArpPacket.ETHERNET_TYPE_IPV4) .payloadBuilder(ipv4) .paddingAtBuild(true) .build(); PcapNetworkInterface nif = Pcaps.getDevByName("eth0"); try (PcapHandle handle = nif.openLive(65536, PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, 10)) { handle.sendPacket(ether); } } }这段代码要解释清楚:correctChecksumAtBuild(true)是在构造包的同时自动计算校验和,省掉手写校验的步骤。paddingAtBuild(true)会让以太网帧不足 64 字节时自动补零,符合最小帧长要求。srcAddr和dstAddr既可以填本机地址,也可以填外部地址;如果是外部地址,注意 Linux 会把这种源地址非本机的包交给网卡发送,但交换机可能会做反向路径过滤,发不出去时先检查rp_filter是否关闭。
4.3 构造 TCP 数据包与 HTTP 请求报文
UDP 无连接,构造相对简单。TCP 则要小心状态机,比如发送一个带 SYN flag 的包,对方回 SYN-ACK 后,这个连接在操作系统里是无状态的,你收不到也维护不了。所以在 Java 封包工具里,构造 TCP 包更多用于测试防火墙的过滤规则,而不是做真正的应用通信。
TcpPacket tcp = new TcpPacket.Builder() .srcAddr(srcIp) .dstAddr(dstIp) .srcPort((short) 5566) .dstPort((short) 80) .sequenceNumber(1000000) .acknowledgmentNumber(0) .dataOffset((byte) 5) .syn(true) .ack(false) .window((short) 8192) .payloadBuilder(new UnknownPacket.Builder() .rawData("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".getBytes())) .correctChecksumAtBuild(true) .build();这里syn(true)表示 SYN 握手,dataOffset((byte) 5)表示 TCP 头长度为 20 字节,没有选项字段。如果带了时间戳,dataOffset必须相应增加,否则接收方会解析错位。实际上,发送这种 SYN 包后系统不会维护连接,后续数据只能靠对方响应,所以更多场景是把完整 HTTP 请求放入 TCP payload 后,用真实Socket发送,而不是用 pcap 裸发。
4.4 发包后的验证:自抓自验
发出去不等于真的发出去了,网络栈可能因为路由、MAC 地址错误丢弃。验证的方法很简单:在同一张网卡上开两个 pcap handle,一个用于发送,另一个用于抓包,把抓到的包再解析出来和发送的字节比对。
tcpdump -i eth0 -nn -X port 53执行发送代码前先开 tcpdump,然后观察有没有期望的 DNS 查询观测包。如果看到了,但目标服务没反应,再看目标 IP 是否可达,ping -c 3 8.8.8.8先确认二层通。还有一个常见坑是 MAC 地址填了网关的却要发给同网段主机,交换机看到目标 MAC 是网关口就会把包转发到路由器,然后被丢弃。所以填 MAC 前先arp -a看路由表,确认下一跳到底是网关还是目的主机。
5. 封包拦截与中间人调试:从监听到阻断的实战
最后把捕获、解析、发送串起来,做成一个能"拦截"的模块。系统重启后,拦截行为要分成两种,别混淆。
5.1 监听式拦截与代理式阻断的区别
抓包本身是一种被动拦截,它能看到所有流量,但不会影响流向,包还是在原来的路径上。真正的"阻断"需要把流量强制拉到你的代码里,处理完再决定放行还是丢弃。在 Java 侧做阻断,最可控的方式是代理式拦截:客户端把请求发到本地代理端口,代理解析后决定放行、修改转发还是直接返回错误。
这种拦截可以用于测试一个"发封包工具"构造的恶意请求是否能被自己的网关识别,也可以用来给老系统加一层临时的接口校验,不需要改业务代码。
5.2 用 Java 写一个可插入的内网 HTTP 封包拦截器
以下代码实现了一个最小的 HTTP 拦截器,监听 8080 端口,先读完整请求头,再决定是否放行:
import java.io.*; import java.net.*; public class HttpInterceptor { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8080); System.out.println("interceptor listening on 8080"); while (true) { try (Socket client = server.accept()) { InputStream in = client.getInputStream(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); int b; while ((b = in.read()) != -1) { buf.write(b); if (buf.toString("ISO-8859-1").contains("\r\n\r\n")) { break; } } String request = buf.toString("ISO-8859-1"); String firstLine = request.split("\r\n")[0]; // 拦截规则:所有包含 "block-me" 的路径直接拒绝 if (request.contains("block-me")) { client.getOutputStream().write( "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n".getBytes()); continue; } // 放行:转发到上游 9000 端口 try (Socket upstream = new Socket("127.0.0.1", 9000)) { upstream.getOutputStream().write(buf.toByteArray()); upstream.getOutputStream().flush(); byte[] resp = upstream.getInputStream().readAllBytes(); client.getOutputStream().write(resp); } } } } }这段代码看起来粗糙但核心逻辑完整。判断请求头结束用的是\r\n\r\n,这是一种简单的封包边界识别;对于有Content-Length的 POST 请求,还需要额外读 body,否则上游会一直等数据。readAllBytes()会等待对端关闭连接才返回,这种写法只适合演示;生产级拦截器需要按Content-Length或 chunked 编码精确读取响应长度。
5.3 拦截器如何与封包工具协作
把上面的拦截器嵌进测试链路后,通常配合三件事:
- 用第 2 章的 pcap 抓包确认客户端确实把请求发到了 8080,而不是直连 9000。
- 用第 3 章的解析代码验证拦截器转发的请求头是否被修改成功。
- 用第 4 章的发送工具直发一个"block-me"请求,看拦截器是否真的会返回 403。
这三步里最容易出错的是第一步,因为很多开发者在客户端没改配置时,流量根本没进代理,拦截器却以为已经拦截了。验证时先在客户端跑一次真实业务,再用抓包工具统计 8080 端口的连接数,至少要看到一条 SYN 记录。
最后一个技巧:不要在ServerSocket.accept()里直接写业务逻辑,否则一个请求没处理完,后面的连接全部排队。正确做法是accept()后丢给ExecutorService,每个连接一个任务,再设置SO_TIMEOUT防止对端断连后线程永久阻塞。Java 的Socket默认没有超时,一旦对端不发数据也不关闭,read()会永远挂住,这是拦截器稳定性的头号杀手。
本文还有配套的精品资源,点击获取