在网络运维和安全测试工作中,检测目标主机的端口开放状态是排查服务可用性、评估网络风险的基础操作。Golang凭借其原生支持的轻量级协程和高效的网络编程能力,成为实现端口扫描工具的热门选择,能够快速完成大量端口的并发检测,同时代码逻辑清晰易于维护。
端口扫描的核心原理
端口扫描的本质是尝试与目标主机的指定端口建立网络连接,根据连接的结果判断端口状态。常见的端口状态分为三类:
- 开放:目标端口有服务监听,成功建立连接
- 关闭:目标端口无服务监听,连接被拒绝
- 过滤:连接超时或无法到达,可能是防火墙拦截等原因
在Golang中,我们可以通过net.DialTimeout函数尝试建立TCP连接,通过连接返回的错误信息和耗时判断端口状态。
基础版单端口扫描实现
首先实现单个端口的检测逻辑,核心是通过设置合理的超时时间,判断连接是否成功建立:
package main
import (
"fmt"
"net"
"time"
)
// checkPort 检测单个端口状态
// host: 目标主机地址,port: 目标端口号,timeout: 连接超时时间
func checkPort(host string, port int, timeout time.Duration) string {
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
// 判断错误类型,区分关闭和过滤状态
if opErr, ok := err.(*net.OpError); ok {
if opErr.Timeout() {
return "过滤"
}
}
return "关闭"
}
defer conn.Close()
return "开放"
}
func main() {
host := "127.0.0.1"
port := 80
timeout := 3 * time.Second
status := checkPort(host, port, timeout)
fmt.Printf("主机 %s 端口 %d 状态:%sn", host, port, status)
}
并发扫描多个端口
单端口扫描效率较低,实际使用中需要同时扫描多个端口,Golang的goroutine可以很方便地实现并发扫描,同时配合通道收集扫描结果:
package main
import (
"fmt"
"net"
"sync"
"time"
)
// PortResult 存储端口扫描结果
type PortResult struct {
Port int
Status string
}
// checkPort 检测单个端口状态
func checkPort(host string, port int, timeout time.Duration, wg *sync.WaitGroup, resultChan chan<PortResult>) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
if opErr, ok := err.(*net.OpError); ok {
if opErr.Timeout() {
resultChan <- PortResult{Port: port, Status: "过滤"}
return
}
}
resultChan <- PortResult{Port: port, Status: "关闭"}
return
}
defer conn.Close()
resultChan <- PortResult{Port: port, Status: "开放"}
}
func main() {
host := "127.0.0.1"
// 扫描1到100端口
startPort := 1
endPort := 100
timeout := 2 * time.Second
var wg sync.WaitGroup
resultChan := make(chan PortResult, endPort-startPort+1)
// 启动并发扫描
for port := startPort; port <= endPort; port++ {
wg.Add(1)
go checkPort(host, port, timeout, &wg, resultChan)
}
// 等待所有扫描完成
wg.Wait()
close(resultChan)
// 输出扫描结果
fmt.Printf("目标主机 %s 端口扫描结果:n", host)
openCount := 0
for res := range resultChan {
if res.Status == "开放" {
fmt.Printf("端口 %d:%sn", res.Port, res.Status)
openCount++
}
}
fmt.Printf("扫描完成,共发现 %d 个开放端口n", openCount)
}
优化扫描体验
上述基础实现还可以做进一步优化,提升工具的实用性和效率:
控制并发数量
无限制启动goroutine可能会导致系统资源占用过高,可以通过有缓冲的通道实现信号量,控制最大并发数:
package main
import (
"fmt"
"net"
"sync"
"time"
)
type PortResult struct {
Port int
Status string
}
func checkPort(host string, port int, timeout time.Duration, sem chan struct{}, wg *sync.WaitGroup, resultChan chan<PortResult>) {
defer wg.Done()
// 获取信号量,控制并发数
sem <- struct{}{}
defer func() { <-sem }()
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
if opErr, ok := err.(*net.OpError); ok {
if opErr.Timeout() {
resultChan <- PortResult{Port: port, Status: "过滤"}
return
}
}
resultChan <- PortResult{Port: port, Status: "关闭"}
return
}
defer conn.Close()
resultChan <- PortResult{Port: port, Status: "开放"}
}
func main() {
host := "127.0.0.1"
startPort := 1
endPort := 200
timeout := 2 * time.Second
// 最大并发数设置为50
maxConcurrent := 50
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
resultChan := make(chan PortResult, endPort-startPort+1)
for port := startPort; port <= endPort; port++ {
wg.Add(1)
go checkPort(host, port, timeout, sem, &wg, resultChan)
}
wg.Wait()
close(resultChan)
fmt.Printf("目标主机 %s 端口扫描结果(1-200):n", host)
for res := range resultChan {
fmt.Printf("端口 %d:%sn", res.Port, res.Status)
}
}
支持命令行参数
可以通过flag包让工具支持自定义目标主机、端口范围、超时时间等参数,提升工具的灵活性:
package main
import (
"flag"
"fmt"
"net"
"sync"
"time"
)
type PortResult struct {
Port int
Status string
}
func checkPort(host string, port int, timeout time.Duration, sem chan struct{}, wg *sync.WaitGroup, resultChan chan<PortResult>) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
address := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
if opErr, ok := err.(*net.OpError); ok {
if opErr.Timeout() {
resultChan <- PortResult{Port: port, Status: "过滤"}
return
}
}
resultChan <- PortResult{Port: port, Status: "关闭"}
return
}
defer conn.Close()
resultChan <- PortResult{Port: port, Status: "开放"}
}
func main() {
// 定义命令行参数
host := flag.String("host", "127.0.0.1", "目标主机地址")
startPort := flag.Int("start", 1, "起始端口")
endPort := flag.Int("end", 100, "结束端口")
timeout := flag.Int("timeout", 2, "超时时间(秒)")
maxConcurrent := flag.Int("concurrent", 50, "最大并发数")
flag.Parse()
connTimeout := time.Duration(*timeout) * time.Second
sem := make(chan struct{}, *maxConcurrent)
var wg sync.WaitGroup
resultChan := make(chan PortResult, *endPort-*startPort+1)
fmt.Printf("开始扫描主机 %s 的 %d-%d 端口...n", *host, *startPort, *endPort)
for port := *startPort; port <= *endPort; port++ {
wg.Add(1)
go checkPort(*host, port, connTimeout, sem, &wg, resultChan)
}
wg.Wait()
close(resultChan)
openPorts := make([]int, 0)
filteredPorts := make([]int, 0)
closedPorts := make([]int, 0)
for res := range resultChan {
switch res.Status {
case "开放":
openPorts = append(openPorts, res.Port)
case "过滤":
filteredPorts = append(filteredPorts, res.Port)
case "关闭":
closedPorts = append(closedPorts, res.Port)
}
}
fmt.Println("n扫描结果统计:")
fmt.Printf("开放端口:%vn", openPorts)
fmt.Printf("过滤端口数量:%dn", len(filteredPorts))
fmt.Printf("关闭端口数量:%dn", len(closedPorts))
}
注意事项
使用端口扫描工具时需要注意以下几点:
- 未经授权对目标主机进行端口扫描可能违反法律法规,仅可在自己拥有权限的主机或授权的测试环境中使用
- 超时时间设置需要合理,过短可能导致误判开放端口为过滤状态,过长会降低扫描效率
- 部分网络环境会对高频端口扫描行为进行拦截,可根据实际情况调整并发数量和扫描间隔
端口扫描工具仅用于合法的网络检测场景,请勿用于非法用途。