Go提供了很好的语言特性,它在性能、安全性和易用性之间取得了平衡。今天我们来学习一下go的基本TCP通信,在理解TCP端口交互的一种有效方法是实现一个端口扫描器,这让我们能更好的理解TCP握手过程以及3种端口状态,从而确定TCP端口是否可用,或者它是否已关闭或使用过滤状态进行响应。
我们需要使用到Go的net包 :net.Dial(network,address string)
network参数是一个字符串,用于标识要启动的连接类型。它不仅适用于TCP,还可以用于创建使用UNIX套接字、UDP和第4层协议的连接。
address string参数是连接的主机
Dial(network,address string) 返回Conn和error,如果连接成功,error将为nil
现在让我们构建一个最简单的端口扫描器
package main
import (
"fmt"
"net"
)
func main() {
_, err := net.Dial("tcp", "scanme.nmap.org:80")
if err == nil {
fmt.Println("Connection successful")
}
}
一次扫描一个端口没什么用,效率太太太低了。TCP端口的范围为1~65535,每次启动程序扫一个端口要扫到明年了。现在我们只扫描512个端口,可以使用for循环。
但是,问题又来了,*net.Dial(network,address string)*第二个参数是一个字符串,用for循环的话会引入整数 i,所以我们要先把它们转换为字符串。有两种方法:
<1>用字符串转换包 strconv
<2>用fmt包中的函数Spring(format string, a…interface{})(这个函数有点类似于C语言中的用法),该函数返回从格式字符串生成的字符串。
package main
import (
"fmt"
"net"
)
func main() {
for i := 1; i <= 512; i++ {
address := fmt.Sprintf("scanme.nmap.org:%d", j)
conn, err := net.Dial("tcp", address)
if err != nil {
return //端口已关闭或已过滤
}
conn.Close()
fmt.Printf("%d open\n", j)
}
}
虽然我们现在已经能扫描出多个端口了,但是仍然属于单一的端口扫描,效率很慢。如果能同时扫描多个端口效果会更快,为此要用到goroutine实现并发执行
package main
import (
"fmt"
"net"
)
func main() {
for i := 1; i <= 512; i++ {
go func(j int) {
address := fmt.Sprintf("scanme.nmap.org:%d", j)
conn, err := net.Dial("tcp", address)
if err != nil {
return
}
conn.Close()
fmt.Printf("%d open\n", j)
}(i)
}
}
但是运行之后问题又来了,程序执行后立马就退出了,这是因为我们为每一个连接启动了一个goroutine,而我们的主goroutine并不知道要等待连接发生。因此代码会在for循环完成迭代后立即退出。要解决这个问题,我们可以用syn包中的WaitGroup,这是一种控制并发的线程安全的方法。
我们在创建WaitGroup后,可以调用Add(int) 方法,它将按所提供的数字递增内部计数器。接下来,Done() 方法将计数器减1。最后,Wait() 会阻止在其中调用它的 goroutine 执行,并且在内部计数器达到 零 之前将不允许进一步执行。
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) {
defer wg.Done()
address := fmt.Sprintf("scanme.namp.org:%d", j)
conn, err := net.Dial("tcp", address)
if err != nil {
fmt.Printf("%d close\n", j)
return
}
conn.Close()
fmt.Printf("%d open\n", j)
}(i)
}
wg.Wait()
}
虽然跟上一个相比有了进步,但但但是,结果仍然是不算正确的。同时扫描过多的主机或端口可能会导致网络或系统限制,造成结果不正确!
为了避免结果的不一致,我们需要使用goroutine池来管理正在进行的并发工作。使用for循环创建一定数量的工人goroutine作为资源池;然后在main()线程中使用通道提供工作。首先创建100个worker,使用int通道将它们打印到屏幕上。继续使用WaitGroup阻塞执行。
package main
import (
"fmt"
"sync"
)
func worker(ports chan int, wg *sync.WaitGroup) {
for p := range ports { //使用range连续地从通道ports接收数据
fmt.Println(p)
wg.Done()
}
}
func main() {
ports := make(chan int, 100)//make创建通道
var wg sync.WaitGroup
for i := 0; i < cap(ports); i++ {
go worker(ports, &wg)
}
for i := 1; i <= 1024; i++ {
wg.Add(1)
ports <- i //将通道ports中的一个端口发送给worker
}
wg.Wait()
close(ports)
}
函数 *worker(int, sync.WaitGroup) 有两个参数:int 类型的通道和指向 WaitGroup 的指针。通道用于接收工作,而 WaitGroup 用于跟踪单个工作的完成情况
运行上面代码后我们发现扫描效率和准确度大大提升,但是端口输出非常乱,为此我们还需要单独使用一个线程将端口扫描的结果传回主线程进行排序,这样做也有一个好处,那就是我们可以摆脱对WaitGroup的依赖,主线程不需要再等待其他线程。
package main
import (
"fmt"
"net"
"sort"
)
func workers(ports, results chan int) {
for p := range ports {
address := fmt.Sprintf("scanme.nmap.org:%d", p)
conn, err := net.Dial("tcp", address)
if err != nil {
results <- 0 //端口关闭将发送0
continue
}
conn.Close()
results <- p //端口开启将发送端口号
}
}
func main() {
ports := make(chan int, 100)
results := make(chan int) //创建一个单独的通道用于将结果从worker线程传递给主线程
var openports []int // 使用切片接收结果
for i := 0; i < cap(ports); i++ {
go workers(ports, results)
}
go func() { // 创建一个单独线程把ports发给workers
for i := 1; i <= 1024; i++ {
ports <- i
}
}()
for i := 0; i < 1024; i++ {
port := <-results
if port != 0 {
openports = append(openports, port)
}
}
close(ports)
close(results)
sort.Ints(openports) //对切片进行排序
for _, port := range openports {
fmt.Printf("%d open\n", port)
}
}
到此,一个高效的端口扫描器实现了,如果我们想让程序执行的更快,只需要增加worker的数量就可以了,但是worker添加过多的话,结果可能就会变得不可靠!!!所以在编写供他人使用的代码时,应该设置一个合理的默认值,以确保结果的可靠性。
参考 : 《Black Hat Go:Go Programming for Hackers and Pentesters》