用Go手撸一个简单的端口扫描工具
最近在搞一个开放的云和边缘拨测平台,需要用到端口扫描工具,开源的闭源的其实很多,nmap 也挺好用。但这些应用都不太满足需求,而且自己最近的确有点手痒,想撸一会儿代码了。首要的需求还是要兼容尽量多的平台和操作系统,其次就是不能有太多的外部库依赖,理所当然的就使用go开撸了。
本文分享的代码已经在并发和内存占用上做了优化,可根据工况调整参数,编译后可以作为一个跨平台的高性能端口扫描工具来使用。需要注意的是,该工具仅用于编程学习,切勿用于非法工况或事务。
使用方法
扫描时会将开放的端口写入到 results.txt
文件中,如果无可用端口此文件将为空。假设编译后的二进制文件名为 portscan
,使用方法如下:
portscan 100 1.1.1.1 2.2.2.2
,以100并发端口的速率扫描两个ip地址portscan 100
,将从ips.txt
读取列表,并以100并发端口的速率扫描这些ip地址
完整代码
作者有点懒,重要的信息都写注释里了。如果对go不太熟悉,可参阅文末来自 Github Copilot 的代码分析。
// Package name: portscan
// Author: Rehiy<https://www.rehiy.com/post/559/>
package main
import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
)
var dialTimeout = 3
var concurrency = 100
func main() {
var err error
var ips []string
// 如果存在第一个命令行参数,使用这个参数作为并发数
if len(os.Args) > 1 {
concurrency, err = strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println("Error: concurrency must be an integer")
os.Exit(1)
}
}
// 如果存在更多的命令行参数,使用这些参数作为 IP 地址
// 否则,从 ips.txt 文件中读取 IP 列表
if len(os.Args) > 2 {
for _, ip := range os.Args[2:] {
if net.ParseIP(ip) == nil {
fmt.Println("Error: Invalid IP address", ip)
os.Exit(1)
}
}
ips = os.Args[2:]
} else {
ips, err = readIPs("ips.txt")
if err != nil {
fmt.Println("Error reading IP list:", err)
os.Exit(1)
}
}
// 创建一个带有缓冲区的 channel 来存储结果
// 在另一个 goroutine 中从 channel 中读取结果并写入文件
results := make(chan string, 100)
defer close(results)
go func() {
file, err := os.OpenFile("results.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Println("Error opening file:", err)
os.Exit(1)
}
defer file.Close()
for result := range results {
if _, err := file.WriteString(result); err != nil {
fmt.Println("Error writing to file:", err)
}
}
}()
// 遍历 IP 列表,对每个 IP 进行端口扫描
// 使用 WaitGroup 和信号量来控制并发数
var wg sync.WaitGroup
sem := make(chan bool, concurrency)
for _, ip := range ips {
for port := 1; port <= 65535; port++ {
wg.Add(1)
sem <- true
go func(ip string, port int) {
defer wg.Done()
defer func() { <-sem }()
addr := buildAddr(ip, port)
if testPort(addr, "tcp") {
// 将结果发送到 channel
results <- addr + "\n"
}
}(ip, port)
}
}
wg.Wait()
}
func readIPs(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var ips []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
ips = append(ips, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
return ips, nil
}
func testPort(addr, network string) bool {
s := time.Duration(dialTimeout) * time.Second
if conn, err := net.DialTimeout(network, addr, s); err == nil {
conn.Close() // 关闭连接,释放资源
fmt.Printf("%s is open\n", addr)
return true
}
return false
}
func buildAddr(ip string, port int) string {
var addr string
if strings.Contains(ip, ":") {
addr = fmt.Sprintf("[%s]:%d", ip, port)
} else {
addr = fmt.Sprintf("%s:%d", ip, port)
}
return addr
}
代码分析
这段代码的主要目的是从命令行参数或文件中获取 IP 地址列表,并设置并发数,然后对这些 IP 地址进行端口扫描。
-
导入了一些必要的包,包括用于网络编程、文件操作、字符串处理、并发控制等的包。
-
定义了两个全局变量:
dialTimeout
和concurrency
。dialTimeout
用于设置连接超时时间,concurrency
用于设置并发数。 -
在
main
函数中,首先定义了一个错误变量err
和一个字符串切片ips
。 -
检查命令行参数。如果存在第一个命令行参数,将其转换为整数并设置为并发数。如果转换失败,打印错误信息并退出程序。
-
如果存在更多的命令行参数,将这些参数作为 IP 地址。对每个 IP 地址进行解析,如果解析失败,打印错误信息并退出程序。
改进方向
这段代码使用了 goroutine 和信号量来并发地扫描每个 IP 地址的所有端口。并发数可以通过命令行参数来设置,这使得代码可以根据系统资源来调整并发数,提高扫描效率。然而,也有一些可以改进的地方:
-
参数解析:代码使用了 os.Args 来解析命令行参数,这种方式比较原始,不支持参数说明、默认值等功能。可以考虑使用像 flag 这样的库来改进参数解析。
-
结果输出:代码将所有的结果都写入同一个文件,如果扫描的 IP 地址很多,结果文件可能会很大。可以考虑将结果分别写入不同的文件,或者提供一个选项来控制是否将结果写入文件。
-
性能优化:代码在扫描每个端口时都会创建一个新的 goroutine,这可能会导致 goroutine 的数量过多。可以考虑使用 goroutine 池来复用 goroutine,减少 goroutine 的创建和销毁开销。
-
错误提示:代码在出现错误时会直接退出程序,这可能会导致用户不清楚发生了什么错误。可以考虑改进错误提示,例如打印更详细的错误信息,或者在出现错误时不退出程序,而是跳过当前的 IP 地址或端口,继续扫描其他的 IP 地址或端口。