Skip to content

实现原理:批量扫描与拓扑

对应代码:pkg/batchscanner.go(并发)+ pkg/mappingdata.go(去重/聚合)+ pkg/chaintopology.go(拓扑)

map-scan 能批量扫数百上千目标并产出测绘摘要。本页讲并发模型、去重、聚合、信任拓扑四个核心。

BatchScanConfig 字段与默认值

字段类型默认值说明
Concurrencyint100并发 worker 数
Timeouttime.Duration5s单目标拨号超时
RateLimitint0(不限速)每秒连接数上限
RetryCountint2重试次数(总尝试 = RetryCount+1)
RetryDelaytime.Duration500ms重试退避
SkipTLSVerifybooltrue默认跳过校验(适配自签/IP)
ServerNamestring""SNI 覆盖

NewBatchScanner 归一化:Concurrency<=0→100;Timeout<=0→5s;RetryCount<0→2;RetryDelay<=0→500ms。RateLimit 不归一化(0 即不限速,newScanRateLimiter(0) 返回 nil)。

并发模型:worker pool

关键设计:

  • worker 数 = min(Concurrency, len(targets))(targets 非空时)
  • jobs channel 无缓冲:每个任务必须有 worker 接收
  • 预分配保序:结果按 index 写回 results[idx],输出顺序与输入一致
  • ctx 取消:关闭 jobs、return results, ctx.Err()

为什么 worker 数 = min(Concurrency, len(targets))

当目标数小于配置并发度时,开再多 worker 也是空跑——NewBatchScannermin 钳制,避免无意义 goroutine。预分配 results 切片 + 按 index 写回是为了输出顺序与输入一致,这对 CSV/JSON 报告很重要。

scanWithRetry 重试逻辑

for attempt := 0; attempt <= RetryCount; attempt++(总尝试 RetryCount+1 次);attempt>0 时先 time.After(RetryDelay);成功立即返回。

scanRateLimiter 令牌桶

scanOnce 单目标采集

BatchScanResult 字段:Target / CertChain []*x509.Certificate(json:-) / CertChainDER [][]byte / TLSVersion uint16 / CipherSuite uint16 / ServerName / ObservedAt / Duration / Error(json:-) / ErrorMessage

目标展开

ScanFromIPRange(L267-285):从 ip.Mask(mask) 起,ipNet.Contains(current) 期间 incrementIP(从末字节自增、进位)。ports 默认 [443]

去重:CertDedup

三种 DedupMethod 与 key 构造(mappingdata.go L25-29, L178-192):

Methodkey type 字符串value 构造
DedupBySPKI"spki_sha256"sha256(cert.RawSubjectPublicKeyInfo) hex
DedupByCertFingerprint"cert_sha256"sha256(cert.Raw) hex
DedupBySerialIssuer"serial_issuer"sha256(SerialNumber.String() + "\x00" + Issuer.String()) hex

NewCertDedup() 无参时默认同时用 DedupByCertFingerprint + DedupBySPKIAdd任一 key 命中即判重;新 cert 把所有 keys 都加入 seen。

三种去重 key 怎么选

  • DedupBySPKI:同公钥的证书视为同一资产(续期会去重)——适合"资产盘点"看唯一公钥数。
  • DedupByCertFingerprint:整张证书相同才去重——适合"证书清单"看唯一证书数。
  • DedupBySerialIssuer:序列号+签发者相同才去重——适合跨 CA 体系比对。 默认双 key(cert+SPKI)任一命中即去重,是"既看证也看公钥"的折中。

聚合:六维分桶

AggregateCertificateAssets(L333-367)按六维分桶:

  • 每桶 key 为空时用 "unknown"
  • ASN key 格式 "AS%d %s"(如 AS13335 Cloudflare
  • 最多 5 个示例 CNif len(samples[kind][key]) < 5 { samples[kind][key][sample] = true }
  • Percentage = count*100/total

organizationKey(L241-260):ToLower + 保留字母数字与空格 + 去停用词 {inc, llc, limited, ltd, corp, corporation, co, company, gmbh, sa}

聚类:并查集

ClusterCertificates(L443-518)默认 sanOverlapThreshold=0.5,三种 cluster reason:

sanJaccard(L520-539):交集/并集,双方都空返回 0。

信任拓扑

BuildTrustChainTopologyFromAssets(chaintopology.go)建图:

trustNodeID 构造(L117-125)

go
if len(cert.Raw) > 0 {
    id = computeHashHex(cert.Raw)  // cert SHA-256 hex
} else {
    id = sha256(Subject.String() + "\x00" + SerialNumber.String())
}

Roots / Leaves 判定(L56-57, L92-97)

遍历每条 chain:chain[certIndex+1]chain[certIndex] 的 issuer → incoming[subjectID]=true, outgoing[issuerID]=true

  • Root = !incoming[node.ID](无人签发它)
  • Leaf = !outgoing[node.ID](它不签发任何人)
  • 自签cert.CheckSignatureFrom(cert)==nil → 加 self-loop edge relation="self_signed"

edgeByKey 去重 key = issuerID + "\x00" + subjectID + "\x00" + relation

拓扑节点/边字段

go
type TrustChainNode struct {
    ID, CertSHA256, SPKISHA256, Subject, CommonName, OrgKey string
    IsCA, SelfSigned bool
    ChainIndexes []int
    Targets []string
}
type TrustChainEdge struct {
    FromIssuerID, ToSubjectID, Relation string
    ChainIndexes []int
    Targets []string
}

离线解析:BatchParseCertificateFiles

config.Concurrency 默认 16bufio.Scanner 缓冲 64KB 初始、16MB 最大。

离线解析的缓冲设计

bufio.Scanner 默认 64KB,遇到超长 PEM(含完整链或大量 SAN)会 ErrTooLong。cert-skills 把上限提到 16MB——足以容纳任何现实证书文件,又不至于无脑占内存。这个值是"够用且不浪费"的工程权衡。

完整测绘摘要

map-scan 一次输出含四块:

这就是测绘三段式的"发现 + 归类"阶段,详见 网络空间测绘

下一步