引言
在 NPS 系列文章中,我们已经探讨了 NPS 的整体架构、服务端核心以及多种代理模式。本篇文章将深入 NPS 的 HTTPS 代理实现。我们将通过分析 nps/server/proxy/https.go 文件,揭示 NPS 如何处理加密的 HTTPS 流量,特别是其对 SNI(Server Name Indication)的支持和多证书管理机制。
HTTPS 代理的需求与挑战
HTTPS 代理比普通的 HTTP 代理更为复杂,因为它涉及到 SSL/TLS 加密。主要挑战包括:
- SSL/TLS 握手:代理服务器需要参与 SSL/TLS 握手过程,解密客户端请求,然后加密转发给目标服务器,或者直接将加密流量转发给目标服务器。
- SNI 支持:为了在同一个 IP 地址上托管多个 HTTPS 网站,客户端在 SSL/TLS 握手时会通过 SNI 扩展告知服务器其要访问的域名。代理服务器需要根据 SNI 信息选择正确的证书。
- 证书管理:代理服务器需要能够管理和加载多个域名的 SSL/TLS 证书。
https.go:HTTPS 代理的实现
https.go 文件定义了 HttpsServer 结构体,它是 NPS 实现 HTTPS 代理的核心。
HttpsServer 结构体
HttpsServer 结构体继承了 httpServer(虽然在 https.go 中没有直接定义 httpServer,但从代码逻辑看,它应该是一个包含 BaseServer 和一些 HTTP 相关功能的结构体),并增加了 HTTPS 特有的字段:
type HttpsServer struct {
httpServer
listener net.Listener
httpsListenerMap sync.Map
hostIdCertMap sync.Map
}
httpServer:继承了处理 HTTP 请求的通用逻辑。listener net.Listener:用于监听传入 HTTPS 连接的网络监听器。httpsListenerMap sync.Map:一个并发安全的 Map,用于存储不同域名的HttpsListener实例。键通常是域名,值是*HttpsListener。hostIdCertMap sync.Map:一个并发安全的 Map,用于存储主机 ID 与其对应的证书文件路径或内容。用于管理和更新证书。
NewHttpsServer() 函数用于创建并初始化一个 HttpsServer 实例。
Start() 方法:启动 HTTPS 监听与 SNI 处理
HttpsServer 的 Start() 方法负责启动 HTTPS 监听,并为每个传入连接进行 SNI 解析和证书选择:
func (https *HttpsServer) Start() error {
conn.Accept(https.listener, func(c net.Conn) {
serverName, rb := GetServerNameFromClientHello(c) // 从 ClientHello 中获取 SNI
r := buildHttpsRequest(serverName) // 构建一个模拟的 HTTP 请求
if host, err := file.GetDb().GetInfoByHost(serverName, r); err != nil {
c.Close()
logs.Debug("the url %s can't be parsed!,remote addr %s", serverName, c.RemoteAddr().String())
return
} else {
if host.CertFilePath == "" || host.KeyFilePath == "" {
logs.Debug("加载客户端本地证书")
https.handleHttps2(c, serverName, rb, r) // 使用客户端本地证书
} else {
logs.Debug("使用上传证书")
// 判断是路径还是证书内容
if strings.Contains(host.CertFilePath, "-----BEGIN") || strings.Contains(host.KeyFilePath, "-----BEGIN") {
logs.Debug("通过上传文件加载证书")
https.cert(host, c, rb, host.CertFilePath, host.KeyFilePath) // 使用上传的证书内容
} else {
logs.Debug("通过路径加载证书")
// 检查证书文件是否存在,并读取内容
// ...
https.cert(host, c, rb, string(cert), string(key)) // 使用上传的证书文件
}
}
}
})
return nil
}
核心流程:
- 获取 SNI:
GetServerNameFromClientHello(c)是一个关键函数,它通过读取客户端发送的 SSL/TLSClientHello消息,从中解析出 SNI 域名 (serverName) 和原始的字节流 (rb)。 - 查找主机配置:根据
serverName从数据库中查找对应的主机配置 (file.Host)。 - 证书选择:
- 如果主机配置中没有指定证书路径 (
CertFilePath和KeyFilePath),则表示使用客户端本地证书(通常是直接转发加密流量)。 - 如果主机配置中指定了证书,NPS 会判断证书是直接上传的内容还是文件路径。
https.cert():这个函数负责加载或更新证书,并为该证书创建一个HttpsListener。
- 如果主机配置中没有指定证书路径 (
HttpsListener:动态创建的 HTTPS 监听器
HttpsListener 是一个自定义的 net.Listener 实现,它允许 NPS 动态地为不同的域名创建和管理 HTTPS 监听器。
type HttpsListener struct {
acceptConn chan *conn.Conn
parentListener net.Listener
}
acceptConn chan *conn.Conn:一个通道,用于接收来自HttpsServer的连接。parentListener net.Listener:底层的 TCP 监听器。
NewHttpsListener() 用于创建 HttpsListener 实例。
cert():证书管理与动态 HTTPS 服务
cert() 函数是 NPS 实现多证书管理和动态 HTTPS 服务的核心:
- 证书缓存与更新:它会检查
hostIdCertMap中是否已经存在该主机的证书。- 如果证书已存在且未更改,则直接复用已有的
HttpsListener。 - 如果证书已更改,则会关闭旧的
HttpsListener,创建新的HttpsListener,并更新 Map。 - 如果是第一次加载证书,则创建新的
HttpsListener。
- 如果证书已存在且未更改,则直接复用已有的
- 创建
HttpsListener:通过NewHttpsListener()创建一个HttpsListener实例。 - 启动 HTTPS 服务:调用
https.NewHttps(l, certFileUrl, keyFileUrl)在新的HttpsListener上启动 HTTPS 服务。 - 将连接传递给
HttpsListener:将原始的客户端连接 (c) 和其原始字节流 (rb) 封装为conn.Conn,并通过l.acceptConn <- acceptConn发送给对应的HttpsListener。HttpsListener会负责后续的 SSL/TLS 握手和数据处理。
handleHttps2() 和 handleHttps():HTTPS 流量转发
这两个函数(handleHttps2 和 handleHttps,虽然 handleHttps 在当前代码中被注释掉了,但其逻辑类似)负责将 HTTPS 流量转发给目标服务:
- 流量和连接数检查:检查客户端的流量和连接数是否超出限制。
- 认证检查:对请求进行认证。
- 获取目标地址:从主机配置中获取目标地址。
- 调用
https.DealClient():最终都通过BaseServer的DealClient()方法来建立与目标服务的连接并进行数据转发。
GetServerNameFromClientHello():SNI 解析
这个辅助函数负责从客户端的 SSL/TLS ClientHello 消息中解析出 SNI 域名。它通过解析 TLS 协议的字节流来实现。
buildHttpsRequest():构建模拟 HTTP 请求
这个辅助函数用于根据 SNI 域名构建一个模拟的 http.Request,以便在 file.GetDb().GetInfoByHost() 中查找对应的主机配置。
总结
nps/server/proxy/https.go 文件展示了 NPS 如何实现强大的 HTTPS 代理功能。其核心在于对 SNI 的支持和动态证书管理。通过解析 ClientHello 消息获取 SNI 域名,NPS 能够根据不同的域名选择和加载对应的 SSL/TLS 证书,并在 HttpsListener 上启动独立的 HTTPS 服务。这使得 NPS 能够在一个端口上代理多个 HTTPS 网站,极大地提升了其在复杂网络环境中的应用能力。
在下一篇文章中,我们将继续探索 NPS 的其他代理模式,例如 HTTP/HTTPS 域名解析。