【Prometheus】【Blackbox Exporter】深入解析 ProbeTCP 函数:如何实现 Go 中的 TCP/SSL 协议探测

在监控和性能分析的场景中,探测远程服务的可达性和安全性是至关重要的。blackbox_exporter 是 Prometheus 项目的一部分,它能够帮助我们在不同协议层(HTTP、HTTPS、TCP、DNS 等)上进行服务监控。本文将详细解读 blackbox_exporter 中的 ProbeTCP 函数,分析如何使用 Go 实现对 TCP 连接的探测、SSL/TLS 协议的支持,以及如何在探测过程中获取并处理 SSL 证书和响应信息。

1. 函数定义

go 复制代码
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 指标

go 复制代码
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 连接

go 复制代码
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. 设置连接超时

go 复制代码
deadline, _ := ctx.Deadline()
if err := conn.SetDeadline(deadline); err != nil {
	logger.Error("Error setting deadline", "err", err)
	return false
}

为了防止代码阻塞太长时间,设置了连接的超时时间(deadline)。这确保了连接不会因等待某些操作而永远挂起。

5. SSL/TLS 连接处理

go 复制代码
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. 处理查询和响应

go 复制代码
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 升级

go 复制代码
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,表示探测成功。

go 复制代码
return true

总结

本文深入分析了 blackbox_exporter 中的 ProbeTCP 函数,详细解释了如何通过 Go 实现 TCP 和 SSL/TLS 协议的探测功能。我们探讨了如何使用 Prometheus 指标暴露 SSL 证书的相关信息、如何处理正则表达式匹配失败以及如何升级到 TLS 连接等技术细节。希望这篇文章能帮助你更好地理解 TCP 协议探测的实现过程及其在监控中的应用。

相关推荐
LuckyLay几秒前
Golang学习笔记_22——Reader示例
笔记·学习·golang·reader·io.reader
C++小厨神2 分钟前
Rust语言的循环实现
开发语言·后端·golang
BinaryBardC43 分钟前
R语言的软件工程
开发语言·后端·golang
半桶水专家2 小时前
go函数的参数怎么设置默认值
开发语言·后端·golang
ByteBlossom6662 小时前
Perl语言的面向对象编程
开发语言·后端·golang
Code侠客行2 小时前
Perl语言的软件开发工具
开发语言·后端·golang
AI向前看2 小时前
Perl语言的语法糖
开发语言·后端·golang
Code花园2 小时前
C#语言的网络编程
开发语言·后端·golang
Linux520小飞鱼3 小时前
T-SQL语言的学习路线
开发语言·后端·golang
小青柑-9 小时前
Go语言中的接收器(Receiver)详解
开发语言·后端·golang