在监控和性能分析的场景中,探测远程服务的可达性和安全性是至关重要的。blackbox_exporter
是 Prometheus 项目的一部分,它能够帮助我们在不同协议层(HTTP、HTTPS、TCP、DNS 等)上进行服务监控。本文将详细解读 blackbox_exporter
中的 ProbeTCP
函数,分析如何使用 Go 实现对 TCP 连接的探测、SSL/TLS 协议的支持,以及如何在探测过程中获取并处理 SSL 证书和响应信息。
1. 函数定义
func ProbeTCP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger *slog.Logger) bool {
ProbeTCP
函数的目的是对指定目标(target
)进行 TCP 连接探测。它会根据配置(module
)发送查询请求,读取响应数据,并在需要时升级到 TLS 连接。
参数说明:
- ctx:上下文,用于控制超时、取消等操作。
- target:目标地址,通常是一个 IP 或域名。
- module:配置模块,包含了与 TCP 连接相关的配置(如查询、响应、TLS 设置等)。
- registry:Prometheus 注册表,用于注册和暴露监控指标。
- logger:日志记录器,用于记录操作信息和错误。
2. 创建 Prometheus 指标
probeSSLEarliestCertExpiry := prometheus.NewGauge(sslEarliestCertExpiryGaugeOpts)
probeSSLLastChainExpiryTimestampSeconds := prometheus.NewGauge(sslChainExpiryInTimeStampGaugeOpts)
probeSSLLastInformation := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "probe_ssl_last_chain_info",
Help: "Contains SSL leaf certificate information",
},
[]string{"fingerprint_sha256", "subject", "issuer", "subjectalternative"},
)
probeTLSVersion := prometheus.NewGaugeVec(
probeTLSInfoGaugeOpts,
[]string{"version"},
)
probeFailedDueToRegex := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "probe_failed_due_to_regex",
Help: "Indicates if probe failed due to regex",
})
registry.MustRegister(probeFailedDueToRegex)
这些代码片段创建了多个 Prometheus 指标,用于收集与 SSL/TLS 相关的数据。具体包括:
- SSL证书到期时间(
probeSSLEarliestCertExpiry
) - SSL链上证书到期时间戳(
probeSSLLastChainExpiryTimestampSeconds
) - SSL证书的详细信息(
probeSSLLastInformation
),包括证书指纹、主题、发行者等 - TLS版本(
probeTLSVersion
) - 正则表达式匹配失败的标记(
probeFailedDueToRegex
)
这些指标可以帮助我们监控目标服务的 SSL 证书状态、TLS 版本以及连接的安全性。
3. 建立 TCP 连接
conn, err := dialTCP(ctx, target, module, registry, logger)
if err != nil {
logger.Error("Error dialing TCP", "err", err)
return false
}
defer conn.Close()
logger.Info("Successfully dialed")
这里尝试通过 dialTCP
函数建立 TCP 连接。如果连接失败,则记录错误并返回 false
。否则,成功建立连接后会调用 defer conn.Close()
确保连接在函数结束时被关闭。
4. 设置连接超时
deadline, _ := ctx.Deadline()
if err := conn.SetDeadline(deadline); err != nil {
logger.Error("Error setting deadline", "err", err)
return false
}
为了防止代码阻塞太长时间,设置了连接的超时时间(deadline
)。这确保了连接不会因等待某些操作而永远挂起。
5. SSL/TLS 连接处理
if module.TCP.TLS {
state := conn.(*tls.Conn).ConnectionState()
registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
probeSSLLastInformation.WithLabelValues(getFingerprint(&state), getSubject(&state), getIssuer(&state), getDNSNames(&state)).Set(1)
}
如果配置要求使用 TLS(module.TCP.TLS
),则会对 TCP 连接进行升级为 TLS 连接,并获取证书的相关信息,包括:
- 证书的最早到期时间
- 使用的 TLS 版本
- SSL 链上证书的到期时间
- 证书的指纹、主题、发行者等信息
这些数据会通过 Prometheus 指标进行暴露。
6. 处理查询和响应
scanner := bufio.NewScanner(conn)
for i, qr := range module.TCP.QueryResponse {
logger.Info("Processing query response entry", "entry_number", i)
send := qr.Send
if qr.Expect.Regexp != nil {
var match []int
// Read lines until one of them matches the configured regexp.
for scanner.Scan() {
logger.Debug("Read line", "line", scanner.Text())
match = qr.Expect.Regexp.FindSubmatchIndex(scanner.Bytes())
if match != nil {
logger.Info("Regexp matched", "regexp", qr.Expect.Regexp, "line", scanner.Text())
break
}
}
if scanner.Err() != nil {
logger.Error("Error reading from connection", "err", scanner.Err().Error())
return false
}
if match == nil {
probeFailedDueToRegex.Set(1)
logger.Error("Regexp did not match", "regexp", qr.Expect.Regexp, "line", scanner.Text())
return false
}
probeFailedDueToRegex.Set(0)
send = string(qr.Expect.Regexp.Expand(nil, []byte(send), scanner.Bytes(), match))
if qr.Labels != nil {
probeExpectInfo(registry, &qr, scanner.Bytes(), match)
}
}
在这个部分,代码循环处理 QueryResponse
配置,发送查询并等待响应。对于每个查询:
- 如果配置了正则表达式(
qr.Expect.Regexp
),则会逐行读取响应数据,直到找到匹配的行。 - 如果没有找到匹配,记录失败并返回
false
。
7. 启动 TLS 升级
if qr.StartTLS {
// Upgrade TCP connection to TLS.
tlsConfig, err := pconfig.NewTLSConfig(&module.TCP.TLSConfig)
if err != nil {
logger.Error("Failed to create TLS configuration", "err", err)
return false
}
if tlsConfig.ServerName == "" {
targetAddress, _, _ := net.SplitHostPort(target) // Had succeeded in dialTCP already.
tlsConfig.ServerName = targetAddress
}
tlsConn := tls.Client(conn, tlsConfig)
defer tlsConn.Close()
// Initiate TLS handshake (required here to get TLS state).
if err := tlsConn.Handshake(); err != nil {
logger.Error("TLS Handshake (client) failed", "err", err)
return false
}
logger.Info("TLS Handshake (client) succeeded.")
conn = net.Conn(tlsConn)
scanner = bufio.NewScanner(conn)
在需要启动 TLS 协议时,代码会创建一个新的 TLS 配置并升级现有的 TCP 连接为 TLS 连接。成功完成 TLS 握手后,继续使用 tlsConn
进行数据交换。
8. 完成探测并返回结果
最后,ProbeTCP
函数会返回 true
,表示探测成功。
return true
总结
本文深入分析了 blackbox_exporter
中的 ProbeTCP
函数,详细解释了如何通过 Go 实现 TCP 和 SSL/TLS 协议的探测功能。我们探讨了如何使用 Prometheus 指标暴露 SSL 证书的相关信息、如何处理正则表达式匹配失败以及如何升级到 TLS 连接等技术细节。希望这篇文章能帮助你更好地理解 TCP 协议探测的实现过程及其在监控中的应用。