Hardisk124/IPK_1
GitHub: Hardisk124/IPK_1
基于原始套接字和 libpcap 的轻量级传输层端口扫描器,支持 TCP SYN 和 UDP 扫描,可报告端口开放、关闭和被过滤三种状态。
Stars: 0 | Forks: 0
# IPK L4 扫描器
一个用 C 语言编写的第四层(传输层)网络端口扫描器。它对目标主机执行 TCP SYN 扫描和 UDP 扫描,并将每个探测的端口报告为 **open**(开放)、**closed**(关闭)或 **filtered**(被过滤)。
## 目录
1. [项目概述](#1-project-overview)
2. [构建说明](#2-build-instructions)
3. [运行说明](#3-run-instructions)
4. [已实现的功能与行为](#4-implemented-features-and-behavior)
5. [设计决策](#5-design-decisions)
6. [测试](#6-testing)
7. [已知限制](#7-known-limitations)
8. [参考资料与来源](#8-references-and-sources)
## 1. 项目概述
**IPK L4 扫描器** (`ipk-L4-scan`) 使用底层原始套接字和数据包捕获 对目标主机上的 TCP 和 UDP 端口进行探测。它同时支持 IPv4 和 IPv6 目标,可以列出可用的网络接口,并接受灵活的端口规范(单端口、范围和逗号分隔的列表)。
**作者:** Martin Turcan
**许可证:** GPL-3.0
## 2. 构建说明
### 前置条件
| 需求 | 版本 |
|-------------|---------|
| GCC | ≥ 9.0 |
| GNU Make | any |
| libpcap | ≥ 1.9 |
| Linux kernel | ≥ 4.x |
在 Debian/Ubuntu 上安装 libpcap:
```
sudo apt-get install libpcap-dev
```
### 编译
```
make # build ipk-L4-scan
make clean # remove compiled objects and executables
```
Makefile 使用 `-Wall -Wextra -std=c11 -pedantic -D_DEFAULT_SOURCE` 进行编译,并链接 `-lpcap`。
## 3. 运行说明
该扫描器需要 **root 权限**(原始套接字需要 `CAP_NET_RAW`)。
### 用法
```
sudo ./ipk-L4-scan -i INTERFACE [-t PORTS] [-u PORTS] [-w TIMEOUT] HOST
sudo ./ipk-L4-scan -i
./ipk-L4-scan -h | --help
```
### 选项
| 标志 | 描述 | 默认值 |
|------|-------------|---------|
| `-i INTERFACE` | 用于扫描的网络接口 **(必填)** | — |
| `-t PORTS` | 要扫描的 TCP 端口 | — |
| `-u PORTS` | 要扫描的 UDP 端口 | — |
| `-w TIMEOUT` | 响应超时时间(毫秒) | `1000` |
| `-h`, `--help` | 打印帮助信息并退出 | — |
| `HOST` | 目标主机名或 IP 地址 **(必填)** | — |
**端口格式示例:**
| 格式 | 示例 | 含义 |
|--------|---------|---------|
| 单端口 | `-t 22` | 仅端口 22 |
| 范围 | `-t 1-1024` | 端口 1 到 1024 |
| 逗号分隔 | `-t 22,80,443` | 端口 22、80 和 443 |
| 混合 | `-t 22,25-27,443` | 端口 22、25、26、27 和 443 |
有效端口号:**1–65535**。
### 示例
**列出活动的网络接口:**
```
./ipk-L4-scan -i
```
```
eth0
lo
```
**扫描 localhost 上的 TCP 端口 22 和 80:**
```
sudo ./ipk-L4-scan -i lo -t 22,80 localhost
```
```
127.0.0.1 22 tcp open
127.0.0.1 80 tcp closed
```
**扫描 UDP 端口 53 (DNS),超时时间为 2 秒:**
```
sudo ./ipk-L4-scan -i eth0 -u 53 -w 2000 8.8.8.8
```
```
8.8.8.8 53 udp open
```
**组合 TCP 和 UDP 扫描:**
```
sudo ./ipk-L4-scan -i eth0 -t 22,443 -u 53 example.com
```
```
93.184.216.34 22 tcp filtered
93.184.216.34 443 tcp open
93.184.216.34 53 udp open
```
**扫描端口范围:**
```
sudo ./ipk-L4-scan -i eth0 -t 20-25 192.168.1.1
```
```
192.168.1.1 20 tcp closed
192.168.1.1 21 tcp closed
192.168.1.1 22 tcp open
192.168.1.1 23 tcp closed
192.168.1.1 24 tcp closed
192.168.1.1 25 tcp closed
```
### 输出格式
每个扫描的端口在标准输出中产生一行:
```
```
其中 `` 为以下之一:
- `open` — 端口接受了探测
- `closed` — 端口主动拒绝了探测
- `filtered` — 在超时时间内未收到响应
## 4. 已实现的功能与行为
### 4.1 TCP SYN 扫描
扫描器构建一个仅设置了 `SYN` 标志的原始 TCP 数据包(半开扫描),并通过原始套接字 (`SOCK_RAW`, `IPPROTO_TCP`) 发送。然后,它使用 libpcap 结合匹配 `tcp and src host and dst port ` 的 BPF 过滤器监听响应。
**端口分类规则:**
| 收到的响应 | 端口状态 |
|-------------------|-------------|
| TCP SYN+ACK | `open` |
| TCP RST | `closed` |
| 无响应(超时) | `filtered` |
扫描在报告 `filtered` 之前会在超时时重试一次(最多总共尝试 2 次)。
### 4.2 UDP 扫描
扫描器向目标端口发送一个空的 UDP 数据报,然后使用匹配 `icmp and src host ` (IPv4) 或 `icmp6 and src host ` (IPv6) 的 BPF 过滤器监听 ICMP/ICMPv6 响应。
**端口分类规则:**
| 收到的响应 | 端口状态 |
|-------------------|-------------|
| ICMP Type 3, Code 3 (端口不可达) | `closed` |
| 无 ICMP 响应(超时) | `open` |
### 4.3 IPv4 和 IPv6 支持
扫描器使用 `AF_UNSPEC` 通过 `getaddrinfo()` 解析目标主机名,这将返回 IPv4 和 IPv6 地址。它会遍历所有解析出的地址并逐一扫描。TCP 校验和使用 IPv4 (`PseudoHeader4`) 和 IPv6 (`PseudoHeader6`) 相应的伪首部格式进行计算。
### 4.4 接口列表
运行 `./ipk-L4-scan -i`(不带接口名称)会调用 `getifaddrs()` 枚举所有活动的 (IFF_UP) 网络接口。重复的名称会被过滤。
### 4.5 端口字符串解析
`parse_port_string()` 支持三种输入格式,可以单独使用,也可以与逗号结合使用:
- 单端口:`"80"` → `[80]`
- 范围:`"20-22"` → `[20, 21, 22]`
- 混合:`"22,80-82,443"` → `[22, 80, 81, 82, 443]`
无效的规范(端口 0、端口 > 65535、反转的范围、非数字输入)将被静默忽略,并产生一个空的端口列表。
## 5. 设计决策
### 5.1 使用原始套接字进行 TCP SYN 扫描
TCP SYN 扫描需要发送构造的数据包而不完成三次握手。操作系统通常会在看到 SYN+ACK 时发送 RST(因为没有调用 `connect()`),但使用原始套接字可以绕过操作系统的 TCP 栈,从而手动构造数据包。这种方法是隐秘端口扫描器的标准做法。
### 5.2 使用 libpcap 进行响应捕获
扫描器没有在原始套接字上使用 `recvfrom()`,而是使用了带有 BPF 过滤器的 libpcap。这提供了在内核级别的精确数据包匹配(通过源 IP 和目标端口),减少了来自无关流量的错误匹配。BPF 过滤发生在数据到达用户空间之前的内核中,这比在应用程序代码中进行过滤更高效。
### 5.3 使用 `poll()` 进行超时处理
扫描器在 pcap 文件描述符 (`pcap_get_selectable_fd()`) 上使用 `poll()`,而不是设置套接字超时或使用带有回调的 `pcap_dispatch()`。这提供了精确的毫秒级超时控制,而不会无限期阻塞,也不需要进行忙轮询。
### 5.4 重试逻辑(2 次尝试)
TCP SYN 和 UDP 扫描在将端口标记为 `filtered` 或 `open` 之前,最多进行 2 次传输尝试。这减少了由瞬时丢包引起的误报,同时避免了无限重试的代价。
### 5.5 TCP 的临时源端口
TCP SYN 数据包使用临时范围内的随机源端口 (49152–65535),因此 BPF 过滤器可以精确地将响应数据包匹配回正确的扫描请求,即使是在按顺序扫描多个端口时。
### 5.6 校验和计算
TCP(和 UDP)校验和是基于伪首部和传输层首部计算的。伪首部的格式在 IPv4(12 字节:源/目标 IP、保留、协议、长度)和 IPv6(40 字节:源/目标 IP6、长度、零、下一头部)之间有所不同。`calculate_checksum()` 函数实现了标准的互联网校验和 (RFC 1071):带有进位回卷的 16 位反码求和。
### 5.7 UDP 默认状态:Open
对于 UDP 扫描,如果在超时时间内没有收到 ICMP 响应,端口将被报告为 `open`。这遵循了 Nmap 和其他扫描器的惯例:没有 ICMP 端口不可达消息是 UDP 端口可能处于开放状态的唯一积极信号,因为许多开放的 UDP 端口根本不会回复空数据报。这意味着 UDP 结果可能包含误报(防火墙可能会静默丢弃 ICMP 回复)。
## 6. 测试
### 6.1 自动化单元测试
项目在 `tests/test_ipk.c` 中包含了全面的单元测试套件。这些测试涵盖了非网络(纯逻辑)功能,因此无需 root 权限和活动网络连接即可运行。
**构建并运行:**
```
make test
```
**预期输出(全部 72 个测试通过):**
```
=== IPK L4 Scanner Unit Tests ===
[parse_port_string]
[PASS] single port: count == 1
[PASS] single port: ports[0] == 80
[PASS] range 1-3: count == 3
[PASS] range 1-3: ports[0] == 1
[PASS] range 1-3: ports[1] == 2
[PASS] range 1-3: ports[2] == 3
[PASS] comma list: count == 3
[PASS] comma list: ports[0] == 22
[PASS] comma list: ports[1] == 80
[PASS] comma list: ports[2] == 443
[PASS] invalid port 0: count == 0
[PASS] port 65536 rejected: count == 0
[PASS] max port 65535: count == 1
[PASS] max port 65535: ports[0] == 65535
[PASS] min port 1: count == 1
[PASS] min port 1: ports[0] == 1
[PASS] NULL port_str: count unchanged
[PASS] range 443-443: count == 1
[PASS] range 443-443: ports[0] == 443
[PASS] inverted range 10-5: count == 0
[parse_port_string (edge cases)]
[PASS] empty string: count == 0
[PASS] non-numeric 'abc': count == 0
[PASS] leading-dash '-80': count == 0
[PASS] incomplete range '80-': count == 0
[PASS] range '0-5' (start=0): count == 0
[PASS] range 65534-65535: count == 2
[PASS] range 65534-65535: ports[0] == 65534
[PASS] range 65534-65535: ports[1] == 65535
[PASS] range '65534-65537' (end > 65535): count == 0
[PASS] trailing comma '80,': count == 1
[PASS] trailing comma '80,': ports[0] == 80
[PASS] mixed '22,25-27,30': count == 5
[PASS] mixed: ports[0] == 22
[PASS] mixed: ports[1] == 25
[PASS] mixed: ports[2] == 26
[PASS] mixed: ports[3] == 27
[PASS] mixed: ports[4] == 30
[port_status_to_string]
[PASS] OPEN -> "open"
[PASS] CLOSED -> "closed"
[PASS] FILTERED -> "filtered"
[calculate_checksum]
[PASS] checksum of all-zero words == 0xFFFF
[PASS] checksum of {0x0045} == ~0x0045
[PASS] checksum of odd-length buffer != 0
[calculate_checksum (edge cases)]
[PASS] checksum of {0xFFFF, 0xFFFF} == 0x0000
[PASS] checksum of {0xFFFF, 0x0001} == 0xFFFE
[PASS] checksum of {0x0800, 0x0035} == 0xF7CA
[parse_arguments]
[PASS] '-h' sets help flag
[PASS] '--help' sets help flag
[PASS] '-i' alone sets list_interfaces flag
[PASS] '-i eth0' sets interface
[PASS] positional HOST is set
[PASS] '-t 80' gives one TCP port
[PASS] '-u 53' gives one UDP port
[PASS] '-w 2000' sets timeout to 2000
[PASS] default timeout is 1000 ms
[PASS] port range 20-22: 3 TCP ports
[PASS] port range 20-22: ports are 20, 21, 22
[PASS] TCP port 80 is set alongside UDP
[PASS] UDP port 53 is set alongside TCP
[PASS] comma-list '-t 22,80,443': 3 ports
[PASS] comma-list: ports are 22, 80, 443
[parse_arguments (edge cases)]
[PASS] host before flags: host == 'localhost'
[PASS] host before flags: interface == 'eth0'
[PASS] host before flags: TCP port 80
[PASS] duplicate -t: count == 1
[PASS] duplicate -t: last port wins (80)
[PASS] UDP range 53-55: count == 3
[PASS] UDP range 53-55: ports are 53, 54, 55
[PASS] '-t 0': tcp_count == 0 (port 0 is invalid)
[PASS] '-t 65536': tcp_count == 0 (port > 65535)
[PASS] '-w 0': timeout == 0
[PASS] '-t abc': tcp_count == 0 (non-numeric port)
=== Results: 72/72 passed ===
```
### 6.2 测试组与原理
#### `parse_port_string` – 正常行为
| 内容 | 原因 | 方法 | 输入 | 预期 | 实际 |
|------|-----|-----|-------|----------|--------|
| 单端口 | 核心用例 | 调用 `parse_port_string("80", ...)` | `"80"` | count=1, ports[0]=80 | ✓ 通过 |
| 端口范围 | 范围扩展必须正确 | 调用 `parse_port_string("1-3", ...)` | `"1-3"` | count=3, ports=[1,2,3] | ✓ 通过 |
| 逗号列表 | 多端口输入 | 调用 `parse_port_string("22,80,443", ...)` | `"22,80,443"` | count=3, ports=[22,80,443] | ✓ 通过 |
| 混合格式 | 组合语法 | 调用 `parse_port_string("22,25-27,30", ...)` | `"22,25-27,30"` | count=5, ports=[22,25,26,27,30] | ✓ 通过 |
| 最小有效端口 (1) | 边界 | 调用 `parse_port_string("1", ...)` | `"1"` | count=1, ports[0]=1 | ✓ 通过 |
| 最大有效端口 (65535) | 边界 | 调用 `parse_port_string("65535", ...)` | `"65535"` | count=1, ports[0]=65535 | ✓ 通过 |
#### `parse_port_string` – 边缘情况
| 内容 | 原因 | 方法 | 输入 | 预期 | 实际 |
|------|-----|-----|-------|----------|--------|
| 端口 0 被拒绝 | 0 不是有效端口 | 调用 `parse_port_string("0", ...)` | `"0"` | count=0 | ✓ 通过 |
| 端口 65536 被拒绝 | 超过最大值 | 调用 `parse_port_string("65536", ...)` | `"65536"` | count=0 | ✓ 通过 |
| 反向范围 | 起始 > 结束 | 调用 `parse_port_string("10-5", ...)` | `"10-5"` | count=0 | ✓ 通过 |
| 空字符串 | 无输入 | 调用 `parse_port_string("", ...)` | `""` | count=0 | ✓ 通过 |
| 非数字 | 无效字符 | 调用 `parse_port_string("abc", ...)` | `"abc"` | count=0 | ✓ 通过 |
| 前导破折号 | 类负数 | 调用 `parse_port_string("-80", ...)` | `"-80"` | count=0 | ✓ 通过 |
| 不完整的范围 | `"80-"` | 调用 `parse_port_string("80-", ...)` | `"80-"` | count=0 | ✓ 通过 |
| 范围起始为 0 | 0 无效 | 调用 `parse_port_string("0-5", ...)` | `"0-5"` | count=0 | ✓ 通过 |
| 范围结束 > 65535 | 超过最大值 | 调用 `parse_port_string("65534-65537", ...)` | `"65534-65537"` | count= | ✓ 通过 |
| 尾随逗号 | 宽松解析 | 调用 `parse_port_string("80,", ...)` | `"80,"` | count=1, ports[0]=80 | ✓ 通过 |
| NULL 输入 | 空值安全 | 在 count=5 时调用 `parse_port_string(NULL, ...)` | `NULL` | count=5 (未更改) | ✓ 通过 |
#### port_status_to_string
| 内容 | 原因 | 输入 | 预期 | 实际 |
|------|-----|-------|----------|--------|
| OPEN 枚举 → 字符串 | 输出正确性 | `OPEN` | `"open"` | ✓ 通过 |
| CLOSED 枚举 → 字符串 | 输出正确性 | `CLOSED` | `"closed"` | ✓ 通过 |
| FILTERED 枚举 → 字符串 | 输出正确性 | `FILTERED` | `"filtered"` | ✓ 通过 |
#### calculate_checksum
| 内容 | 原因 | 输入 | 预期 | 实际 |
|------|-----|-------|----------|--------|
| 全零缓冲区 | 基本情况 | `{0,0,0,0}` (8 字节) | `0xFFFF` | ✓ 通过 |
| 单字 | 最小情况 | `{0x0045}` | `~0x0045` | ✓ 通过 |
| 奇数字节缓冲区 | 填充逻辑 | `{0x00,0x01,0x02}` | ≠ 0 | ✓ 通过 |
| 全一(进位回卷)| 进位传播 | `{0xFFFF,0xFFFF}` | `0x0000` | ✓ 通过 |
| 单次进位 | 进位回卷 | `{0xFFFF,0x0001}` | `0xFFFE` | ✓ 通过 |
| 已知值 | 确定性 | `{0x0800,0x0035}` | `0xF7CA` | ✓ 通过 |
#### parse_arguments
| 内容 | 原因 | 输入 argv | 预期 | 实际 |
|------|-----|------------|----------|--------|
| `-h` 标志 | 帮助路径 | `["-h"]` | `args.help == true` | ✓ 通过 |
| `--help` 标志 | 帮助路径 | `["--help"]` | `args.help == true` | ✓ 通过 |
| 仅 `-i` | 接口列表 | `["-i"]` | `args.list_interfaces == true` | ✓ 通过 |
| `-i eth0` + host | 常规扫描 | `["-i","eth0","-t","80","localhost"]` | interface="eth0", host="localhost", tcp=[80] | ✓ 通过 |
| `-u 53` | UDP 端口 | `["-i","eth0","-u","53","localhost"]` | udp=[53] | ✓ 通过 |
| `-w 2000` | 自定义超时 | `[...,"-w","2000",...]` | timeout=2000 | ✓ 通过 |
| 默认超时 | 未指定 `-w` | `["-i","eth0","-t","80","localhost"]` | timeout=1000 | ✓ 通过 |
| 端口范围 | `20-22` | `[...,"-t","20-22",...]` | tcp=[20,21,22] | ✓ 通过 |
| TCP + UDP | 组合 | `[...,"-t","80","-u","53",...]` | tcp=[80], udp=[53] | ✓ 通过 |
| 逗号分隔的端口列表 | `22,80,443` | `[...,"-t","22,80,443",...]` | tcp=[22,80,443] | ✓ 通过 |
| 标志前有主机 | 参数顺序 | `["localhost","-i","eth0","-t","80"]` | host="localhost" | ✓ 通过 |
| 重复的 `-t` | 后者覆盖前者 | `[...,"-t","22","-t","80",...]` | tcp=[80] | ✓ 通过 |
| `-t 0` 被拒绝 | 无效端口 | `[...,"-t","0",...]` | tcp_count=0 | ✓ 通过 |
| `-t 65536` 被拒绝 | 无效端口 | `[...,"-t","65536",...]` | tcp_count=0 | ✓ 通过 |
| `-t abc` 被拒绝 | 非数字 | `[...,"-t","abc",...]` | tcp_count=0 | ✓ 通过 |
### 6.3 测试环境
| 组件 | 版本 / 详情 |
|-----------|-----------------|
| 操作系统 | Ubuntu 22.04 LTS (x86_64) |
| 编译器 | GCC 11.4.0 |
| libpcap | 1.10.1 |
| 标准 | C11 (`-std=c11`) |
| 需要 Root | 否(单元测试仅测试纯逻辑) |
## 7. 已知限制
- **需要 root 权限。** TCP SYN 扫描使用原始套接字 (`SOCK_RAW`),这需要 `CAP_NET_RAW` 或以 root 身份运行。
- **仅限 Linux。** 该实现依赖于 Linux 特定的头文件(`netinet/ip.h`、`netinet/tcp.h`、`netinet/ip_icmp.h`)和 pcap 可选文件描述符 API。
- **顺序扫描。** 端口逐一扫描。与 Nmap 等并行扫描器相比,扫描大范围(例如 1–65535)速度较慢。
- **无服务检测。** 扫描器仅确定端口 open/closed/filtered 的状态;它不识别运行的服务或其版本。
- **重复的 `-t` / `-u` 覆盖。** 多次指定 `-t`(或 `-u`)会导致后者的值覆盖前者;不会引发错误。
## 8. 参考资料与来源
1. **RFC 793 – Transmission Control Protocol** – TCP 首部格式及 SYN/RST 标志语义。
https://datatracker.ietf.org/doc/html/rfc793
2. **RFC 768 – User Datagram Protocol** – UDP 数据包格式。
https://datatracker.ietf.org/doc/html/rfc768
3. **RFC 792 – Internet Control Message Protocol (ICMP)** – ICMP Type 3 (目标不可达) 和 Code 3 (端口不可达)。
https://datatracker.ietf.org/doc/html/rfc792
4. **RFC 1071 – Computing the Internet Checksum** – `calculate_checksum()` 中使用的算法。
https://datatracker.ietf.org/doc/html/rfc1071
5. **RFC 4443 – ICMPv6 for IPv6** – 用于 IPv6 UDP 扫描的 ICMPv6 消息格式。
https://datatracker.ietf.org/doc/html/rfc4443
6. **pcap(3) / libpcap documentation** – 数据包捕获 API,BPF 过滤器,`pcap_get_selectable_fd()`。
https://www.tcpdump.org/manpages/pcap.3pcap.html
7. **Nmap Network Scanning (Gordon "Fyodor" Lyon)** – 端口扫描技术和端口状态分类约定的参考。
https://nmap.org/book/
8. **Linux `raw(7)` man page** – 原始套接字行为,带有 `IPPROTO_TCP` 的 `SOCK_RAW`。
https://man7.org/linux/man-pages/man7/raw.7.html
9. **Linux `getifaddrs(3)` man page** – 接口枚举 API。
https://man7.org/linux/man-pages/man3/getifaddrs.3.html
标签:CDN识别, certspotter, DNS枚举, DNS查询, GPL-3.0, Groq, IPk L4 Scanner, IPv4, IPv6, L4扫描, PowerShell, SOCK_RAW, TCP SYN扫描, 主机探测, 传输层扫描, 原始套接字, 嗅探/抓包, 多线程网络编程, 客户端加密, 扫描与发现, 插件系统, 数据统计, 目录遍历, 端口扫描, 端口状态检测, 网络协议分析, 网络安全, 网络安全工具, 网络扫描器, 隐私保护