应使用 net.DialTimeout 而非 net.Dial 判断端口开放状态,避免无响应时长时间阻塞;超时建议内网1秒、公网2秒,并按错误类型分类处理;并发扫描须用带缓冲 channel 限流,防止资源耗尽。

用 net.DialTimeout 判断端口是否开放,别用 net.Dial
直接调用 net.Dial 会卡死在无响应端口上——比如目标主机宕机、防火墙静默丢包时,它可能等几十秒才返回错误。生产环境必须用 net.DialTimeout 控制单次探测上限。
超时设太短(如 100ms)容易漏报:高延迟链路或负载重的服务可能刚好慢半拍;设太长(如 5s)又拖垮整体扫描速度。实测 1–2 秒最平衡,内网建议 1s,跨公网可放宽到 2s。
注意错误分类:
• err != nil && netErr.Timeout() → 标记为 “filtered”(被过滤,非关闭)
• err != nil && strings.Contains(err.Error(), "connection refused") → 明确是 “open”(服务监听但拒绝连接)
• 其他 error(如 "no route to host")→ 主机不可达,跳过该端口即可
并发控制必须用带缓冲的 chan,别裸开 goroutine
看到示例里写 for port := range ports { go scanPort(...) } 就警觉:这会瞬间启动成千上万个 goroutine,极易触发系统限制(如 too many open files)或被目标防火墙限流。
正确做法是用信号量模式:
• 声明 sem := make(chan struct{}, 100) 控制最大并发数
• 每个 goroutine 扫描前先 sem ,扫完后
• 并发数不是越多越好:Linux 默认文件描述符限制常为 1024,设 100 是安全起点;万兆内网可试 200–300,公网请压到 20–50
• 别忘了 close(ports) 和 worker 中检测 port, ok := ,否则 goroutine 泄漏
批量扫 CIDR 网段时,先做 IP 合法性过滤
用 net.ParseCIDR 解析 "192.168.1.0/24" 后遍历所有 IP,看似方便,但会扫到保留地址(如 127.0.0.0/8、10.0.0.0/8)甚至广播地址,既无意义又可能触发告警。
实操建议:
• 用 ip.IsUnspecified() || ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() 过滤掉本地/无效地址
• 对每个 IP 单独加并发限制(比如每 IP 最多 50 个端口协程),避免某台主机成为瓶颈
• 若扫大网段(如 /16),务必加随机 jitter(如 time.Sleep(time.Millisecond * time.Duration(rand.Intn(10)))),降低被识别为扫描工具的概率
结果输出要区分 “open” 和 “filtered”,别全标 “closed”
很多初版工具把超时和拒绝都打印成 “closed”,这是典型误判。
真实网络中:
• connection refused = 服务进程在监听,端口明确开放
• i/o timeout = 中间设备(防火墙、ACL、NAT)拦截或丢包,状态应为 “filtered”
• 两者运维意义完全不同:“open” 要查服务健康,“filtered” 得查网络策略
简单增强:建个映射表 portServiceMap = map[int]string{22: "SSH", 80: "HTTP", 443: "HTTPS"},输出时带上服务名,比纯数字直观得多
立即学习“go语言免费学习笔记(深入)”;
真正难的不是写通逻辑,而是把超时判断、并发节流、错误归因、IP 合法性这四块边界条件理清楚——漏掉任一环,工具在复杂网络里就不可靠。










