使用Go编写一个简单的TCP端口扫描器

最近在 b 站上刷到了一个用 go 做工具的视频,正巧之前下到过相关的电子书,这里正好学习学习。

基础学习

测试端口可用性

正常 nmap 扫描测试 1-1024 端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
nmap -p 1-1024 scanme.nmap.org
Starting Nmap 7.94 ( https://nmap.org ) at 2024-08-19 18:37 中国标准时间
Nmap scan report for scanme.nmap.org (45.33.32.156)
Host is up (0.22s latency).
Not shown: 1016 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
79/tcp filtered finger
80/tcp open http
111/tcp filtered rpcbind
135/tcp filtered msrpc
137/tcp filtered netbios-ns
139/tcp filtered netbios-ssn
445/tcp filtered microsoft-ds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"net"
)

func main() {
for i := 1; i <= 1024; i++ {
address := fmt.Sprintf("scanme.nmap.org:%d", i)
conn, err := net.Dial("tcp", address)
fmt.Println("Trying to connect to ", address)
if err != nil {
continue
}
err1 := conn.Close()
if err1 != nil {
return
}
fmt.Printf("port %d is opened\n", i)
}
}

由于网络等等原因,这需要不少时间,大概 1min 才能从 0 跑到 22 端口。
Clip_2024-08-19_18-46-45.png
但这样是可以得到预期的结果的。

执行非并发扫描

如果按照简单的思路使用 Goroutine 来执行匿名函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"net"
"sync"
)

func main() {
for i := 1; i <= 1024; i++ {
go func(j int) {
address := fmt.Sprintf("scanme.nmap.org:%d", i)
conn, err := net.Dial("tcp", address)
fmt.Println("Trying to connect to ", address)
if err != nil {
return
}
err1 := conn.Close()
if err1 != nil {
return
}
fmt.Printf("port %d is opened\n", i)
}(i)
}
}

但这样只会立即退出,为每个连接都创建一个 goroutine,主 goroutine 不会等待连接发生,这个是不是很眼熟,如果你在 python 的某些情况下这样写的话,也会出现这种情况。然后 for 循环立即退出。
这里可以使用 sync 包的WaitGroup方法,来控制并发线程的安全,通过调用计数器,来保证程序不会在完成之前退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"net"
"sync"
)

func main() {
var wg sync.WaitGroup
for i := 1; i <= 1024; i++ {
wg.Add(1)
go func(j int) {
// * 当前函数执行完毕后,wg.Done()会将wg的计数器减1
defer wg.Done()
address := fmt.Sprintf("scanme.nmap.org:%d", i)
conn, err := net.Dial("tcp", address)
fmt.Println("Trying to connect to ", address)
if err != nil {
return
}
err1 := conn.Close()
if err1 != nil {
return
}
fmt.Printf("port %d is opened\n", i)
}(i)
}
wg.Wait()
}

但是似乎这个结果还是有问题。只能扫出来部分正确结果,而且有误报。

使用工人池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"sync"
)

// 定义名为worker的函数,用于接收整数类型的channel,并等待一个WaitGroup
func worker(ports chan int, wg *sync.WaitGroup) {
// 从ports channel接收整数
for p := range ports {
// 打印从channel接收到的整数
fmt.Println(p)
wg.Done()
}
}

func main() {
// 创建一个缓存大小为100的整数channel
ports := make(chan int, 100)
var wg sync.WaitGroup
// 启动cap(ports)个worker协程来并发处理ports channel中的数据
for i := 0; i < cap(ports); i++ {
// 启动一个worker协程
go worker(ports, &wg)
}
for i := 1; i <= 1024; i++ {
// 每生成一个任务,调用wg的Add方法,通知WaitGroup有一个新任务加入
wg.Add(1)
// 将整数i发送到ports channel
ports <- i
}
// 等待所有任务完成
wg.Wait()
// 关闭ports channel
close(ports)
}

之前好久没用 go 了,这里简单介绍下基础代码。但是仅仅使用一个通道会导致扫描结果是乱序,可以通过多通道来将打印之前的结果排序,这样也不再需要 WaitGroup,通过对传输结果的计数就可以判断是否完成。
Clip_2024-08-19_20-28-43.png
但实际上这并没有显示出我的全部结果,因此后面还需要再进行修改。

简单完善

后面我添加了解析命令行输入等基础操作,做成了一个简单的 TCP 扫描 demo,但是速度太慢了,扫几个端口就要 20 秒。
Clip_2024-08-21_19-45-36.png
后面再完善完善放个 demo,陆续完善下这篇文章。