第一章:Go标准库crypto/tls包Handshake超时漏洞全景剖析
Go 标准库 crypto/tls 包在 TLS 握手阶段长期存在隐式超时缺失问题:当客户端发起握手但服务端响应异常缓慢(如网络拥塞、恶意延迟或半开连接),(*tls.Conn).Handshake() 方法将无限期阻塞,直至底层 TCP 连接被系统级中断(如 FIN/RST)或进程被强制终止。该行为违反最小权限与防御性编程原则,导致服务端 goroutine 泄漏、连接池耗尽及拒绝服务风险。
漏洞触发条件
- 客户端调用
conn.Handshake()且未设置上层上下文超时; - 服务端在 ServerHello 后停滞(例如不发送 Certificate 或 Finished 消息);
- 底层
net.Conn本身未配置SetDeadline或SetReadDeadline。
实际复现步骤
以下代码可稳定复现阻塞行为(需配合模拟慢响应服务端):
// 客户端:无超时的 Handshake 调用(危险示例)
conn, _ := tls.Dial("tcp", "localhost:8443", &tls.Config{InsecureSkipVerify: true})
// 此处将永久阻塞,若服务端不完成握手
err := conn.Handshake() // ⚠️ 无内置超时机制
if err != nil {
log.Fatal(err)
}
安全加固方案
必须显式引入超时控制,推荐两种方式:
- 上下文超时(推荐):使用
tls.DialContext替代tls.Dial; - 连接层 deadline:在
net.Conn上预设读写截止时间。
| 方案 | 优点 | 注意事项 |
|---|---|---|
tls.DialContext(ctx, ...) |
与 Go 生态超时模型一致,自动取消 goroutine | 需 Go 1.19+,旧版本需降级处理 |
conn.SetReadDeadline(time.Now().Add(10*time.Second)) |
兼容所有 Go 版本 | 需在 Dial 后、Handshake 前调用 |
补丁演进关键点
- Go 1.18 开始,
tls.Conn内部已支持部分上下文感知,但Handshake()方法仍未默认集成; - 社区提案 CL 52173 提议为
Handshake()增加context.Context参数,尚未合入主线; - 当前最佳实践:始终通过
tls.DialContext初始化连接,并避免后续裸调Handshake()。
第二章:TLS握手生命周期与Go HTTP客户端超时机制解耦分析
2.1 crypto/tls.Conn.Handshake()的阻塞行为与底层系统调用链追踪
Handshake() 是 TLS 握手的入口,其阻塞本质源于底层 net.Conn.Read() 和 Write() 的同步 I/O 行为。
阻塞触发点
// 源码简化路径:crypto/tls/conn.go#Handshake
func (c *Conn) Handshake() error {
if err := c.handshake(); err != nil {
return err // 此处可能阻塞在 c.in.readFromSrc()
}
return nil
}
c.in.readFromSrc() 最终调用 c.conn.Read() —— 即底层 net.Conn(如 *net.TCPConn)的阻塞读,依赖 read(2) 系统调用。
系统调用链路
| 层级 | 调用节点 | 关键行为 |
|---|---|---|
| Go 标准库 | net.(*conn).Read |
封装 syscall.Read |
| syscall 包 | syscall.Syscall |
触发 read(2) 系统调用 |
| 内核 | sys_read() → tcp_recvmsg() |
等待数据就绪或超时 |
调用流图
graph TD
A[Conn.Handshake()] --> B[c.handshake()]
B --> C[c.in.readFromSrc()]
C --> D[c.conn.Read()]
D --> E[syscall.Read]
E --> F[read syscall]
F --> G[kernel TCP receive queue]
2.2 net/http.DefaultClient.Timeout对TCP连接、TLS握手、HTTP请求三阶段的覆盖边界实证
net/http.DefaultClient.Timeout 并非全局超时控制器,其作用范围仅限于整个请求生命周期的总耗时上限,从 RoundTrip 调用开始计时,到响应体读取完成(或错误)结束。
实际覆盖阶段边界
- ✅ 覆盖:TCP 连接建立 + TLS 握手 + HTTP 请求发送 + 响应头接收
- ❌ 不覆盖:响应体流式读取(如
resp.Body.Read())——需单独设置resp.Body的ReadTimeout
关键验证代码
client := &http.Client{
Timeout: 5 * time.Second,
}
req, _ := http.NewRequest("GET", "https://httpbin.org/delay/3", nil)
resp, err := client.Do(req) // 计时起点:Do() 调用时刻
此处
Timeout=5s会中断:若 TCP 连接耗时 2s、TLS 握手 1.8s、请求发送+响应头接收 1.3s(累计 5.1s),则触发context deadline exceeded。但若响应体需 10s 流式解析,Do()已返回成功,Timeout不再干预。
阶段耗时归属对照表
| 阶段 | 是否受 DefaultClient.Timeout 约束 |
说明 |
|---|---|---|
| DNS 解析 | 否 | 由 net.Resolver 控制 |
| TCP 连接建立 | 是 | 包含在 RoundTrip 总计时内 |
| TLS 握手 | 是 | 同上 |
| 请求发送 | 是 | |
| 响应头接收 | 是 | |
| 响应体读取 | 否 | 需 http.Response.Body 单独设 io.ReadCloser 超时 |
graph TD
A[client.Do req] --> B[TCP Dial]
B --> C[TLS Handshake]
C --> D[Send Request]
D --> E[Read Response Headers]
E --> F[Return *http.Response]
F --> G[resp.Body.Read?]
style G stroke:#ff6b6b,stroke-width:2px
2.3 Go 1.18–1.22中tls.Config.Timeouts字段缺失导致的语义空白与设计惯性
Go 1.18 至 1.22 的 crypto/tls 包中,tls.Config 始终未提供原生 Timeouts 字段(如 HandshakeTimeout、IdleTimeout),迫使开发者在 net.Listen 或 http.Server 层重复配置,造成语义割裂。
超时配置的分散实践
http.Server.ReadTimeout/WriteTimeout仅作用于应用层读写tls.Config无法声明握手超时,依赖底层net.Conn.SetDeadlinehttp2.Server更完全忽略 TLS 握手阶段控制
典型补救代码
// 手动包装 listener 实现握手超时(Go 1.21)
ln, _ := net.Listen("tcp", ":443")
tlsLn := tls.NewListener(&timeoutListener{ln}, config)
// timeoutListener.Read() 中调用 conn.SetReadDeadline()
该方案绕过 tls.Config,暴露底层连接生命周期,破坏封装性;SetReadDeadline 对 TLS 握手无粒度控制,易触发 EOF 而非 net/http.ErrServerClosed。
超时能力映射表
| 超时类型 | 可配置位置 | TLS 层可见性 |
|---|---|---|
| Handshake | net.Conn + 自定义 |
❌ |
| Idle (keep-alive) | http.Server.IdleTimeout |
✅(HTTP/1.1) |
| Read/Write | http.Server.*Timeout |
⚠️(仅应用数据) |
graph TD
A[Client Hello] --> B{TLS Config}
B -->|无 Timeout 字段| C[net.Conn.SetDeadline]
C --> D[模糊超时边界]
D --> E[握手失败误判为 I/O error]
2.4 复现漏洞:构造无响应SNI服务器+自定义tls.Dialer验证Handshake永久阻塞
构建哑铃式SNI服务器
使用 net.Listen("tcp", ":443") 启动监听,仅接受连接、绝不读取或响应 TLS 握手数据(包括 ClientHello 中的 SNI 扩展):
ln, _ := net.Listen("tcp", ":443")
for {
conn, _ := ln.Accept()
// 不调用 conn.Read() —— 故意挂起 TLS Handshake
}
逻辑分析:TLS 客户端在发送 ClientHello 后,会等待 ServerHello;该服务器不消费任何字节,导致 tls.Conn.Handshake() 在 readServerHello 阶段无限阻塞。关键参数:conn 未设置 SetReadDeadline,默认无超时。
自定义 Dialer 触发阻塞
dialer := &tls.Dialer{
Config: &tls.Config{ServerName: "example.com"},
}
conn, err := dialer.Dial("tcp", "localhost:443", nil)
// 此处永久 hang,直至 TCP RST 或系统级超时
验证阻塞行为的关键指标
| 指标 | 值 | 说明 |
|---|---|---|
conn.LocalAddr() |
有效地址 | 连接已建立(SYN/SYN-ACK 完成) |
err |
nil(永不返回) |
Handshake 未完成,Dial 返回前即阻塞 |
| goroutine 状态 | IO wait |
在 read() 系统调用中休眠 |
graph TD
A[Client: tls.Dial] --> B[Send ClientHello]
B --> C[Server: Accept but ignore]
C --> D[Client blocks in readServerHello]
D --> E[OS socket recv → indefinite wait]
2.5 生产环境泄漏模式识别:pprof goroutine dump中tls.(*Conn).Handshake的堆积特征
当 TLS 握手协程持续堆积,goroutine dump 中高频出现 tls.(*Conn).Handshake 调用栈,往往指向连接管理失当。
常见诱因
- 客户端频繁建连但未复用连接(缺少
http.Transport.MaxIdleConns配置) - 服务端 TLS 证书验证耗时突增(如 OCSP Stapling 超时阻塞)
- 网络中间件(如 LB)未正确透传
Connection: keep-alive
典型 pprof 片段
goroutine 1234 [select]:
crypto/tls/conn.go:1023 +0x1a5
net/http.(*persistConn).addTLS(0xc000ab1200)
net/http/transport.go:1522 +0x2b9
此栈表明协程卡在
addTLS的select等待中——本质是tls.Conn.Handshake()调用未返回,常因底层Read()阻塞于无响应的 TLS ClientHello 或证书链校验超时。
关键诊断指标
| 指标 | 健康阈值 | 异常含义 |
|---|---|---|
runtime.NumGoroutine() |
>2000 且稳定不降 | |
http_client_handshake_seconds_sum |
P99 > 5s 暗示 TLS 层阻塞 |
graph TD
A[HTTP Client] -->|发起TLS握手| B[tls.(*Conn).Handshake]
B --> C{证书验证/密钥交换}
C -->|成功| D[建立加密通道]
C -->|超时/失败| E[goroutine 挂起等待]
E --> F[pprof dump 中持续堆积]
第三章:Go标准库源码级漏洞根因定位
3.1 源码路径分析:src/crypto/tls/handshake_client.go中handshakeOnce的无超时循环逻辑
handshakeOnce 是 TLS 客户端握手的核心协调器,其关键行为在于不主动设超时的 for-select 循环:
for !c.handshaked {
select {
case <-c.handshakeComplete:
return c.handshakeErr
case <-c.cancelCtx.Done():
return c.cancelCtx.Err()
}
}
该循环依赖外部信号(完成通道或上下文取消)退出,自身无 time.After 或重试计时器,易因网络阻塞或服务端失联导致永久挂起。
风险触发场景
- 服务端未响应 ClientHello(如防火墙拦截)
- 中间设备丢弃 ChangeCipherSpec 报文
handshakeComplete通道未被正确关闭
超时机制缺失对比表
| 组件 | 是否内置超时 | 依赖方 |
|---|---|---|
handshakeOnce |
❌ 否 | 调用方需包裹 context.WithTimeout |
Dialer.DialContext |
✅ 是 | net.Dialer.Timeout |
关键修复路径
- 所有
tls.Dial/tls.ClientConn.Handshake()调用必须显式传入带超时的context.Context - Go 1.22+ 推荐使用
tls.Dialer.DialContext替代裸tls.ClientConn手动握手
3.2 net/http/transport.go中dialTLSContext未透传context.Deadline至tls.Conn.Handshake调用栈
dialTLSContext 在建立 TLS 连接时仅将 ctx 传递给 net.DialContext,但后续 tls.ClientConn.Handshake() 调用完全脱离上下文控制:
// src/net/http/transport.go(Go 1.22)
func (t *Transport) dialTLSContext(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := t.dialContext(ctx, network, addr) // ✅ 受 Deadline 约束
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, t.TLSClientConfig)
// ❌ Handshake() 不接收 ctx,无法响应 deadline 到期
err = tlsConn.Handshake() // 阻塞,无超时感知
if err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
}
该设计导致 TLS 握手阶段无法响应 context.Deadline,引发长连接阻塞风险。
关键影响链路
context.WithTimeout(...)→dialTLSContext→net.DialContext(受控)→tlsConn.Handshake()(失控)- TLS 握手耗时 > Deadline 时,goroutine 挂起,资源泄漏
对比:Go 1.23 改进方向(草案)
| 组件 | 是否感知 Context Deadline | 备注 |
|---|---|---|
net.DialContext |
✅ | 基于 syscall timeout 实现 |
tls.Conn.Handshake |
❌(当前) | 依赖底层 Read/Write 的 SetDeadline,但未联动 ctx.Done() |
http.Transport.DialTLSContext(提案) |
⚠️(待实现) | 需重构为 HandshakeContext |
graph TD
A[context.WithDeadline] --> B[dialTLSContext]
B --> C[net.DialContext<br>✅ Deadline-aware]
C --> D[tls.Client]
D --> E[tlsConn.Handshake<br>❌ No ctx, no deadline]
3.3 Go官方issue #50917与CL 456232补丁的演进局限性解读
核心问题定位
issue #50917 暴露了 net/http 中 ResponseWriter 在长连接场景下,WriteHeader() 调用后仍允许写入响应体但未同步刷新状态的竞态风险。
补丁CL 456232的关键变更
// CL 456232 patch snippet (simplified)
func (w *response) WriteHeader(code int) {
if w.wroteHeader {
return // early return — no error, no sync
}
w.status = code
w.wroteHeader = true // volatile write, no memory barrier
}
⚠️ 逻辑分析:该补丁仅增加写标记,但未对 w.status 写入施加 sync/atomic.StoreInt32 或 runtime.WriteBarrier 语义,导致在多 goroutine + weak-memory 架构(如 ARM64)下,status 可能延迟对其他 goroutine 可见。
局限性体现
- ❌ 未解决内存可见性问题
- ❌ 未覆盖
Hijack()后的并发写场景 - ✅ 防止重复调用
WriteHeader(基础防护)
| 维度 | CL 456232 支持 | 真实生产需求 |
|---|---|---|
| 重复 Header 写入防护 | ✔️ | ✔️ |
| 跨 goroutine 状态同步 | ❌ | ✔️ |
| Hijacked 连接一致性 | ❌ | ✔️ |
后续演进瓶颈
graph TD
A[issue #50917 报告] --> B[CL 456232 快速修复]
B --> C[暴露内存模型缺陷]
C --> D[需重构 response 状态机+原子操作]
D --> E[破坏兼容性:WriteHeader 返回 error?]
第四章:工程化防御与兼容性修复方案
4.1 Context-aware TLS Dialer:基于context.WithTimeout封装tls.Dial的生产就绪实现
在高并发服务中,原生 tls.Dial 缺乏上下文取消能力,易导致连接泄漏与超时不可控。生产环境需将其与 context.Context 深度集成。
核心封装逻辑
func ContextualTLSDial(ctx context.Context, network, addr string, config *tls.Config) (net.Conn, error) {
dialer := &net.Dialer{KeepAlive: 30 * time.Second}
conn, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, config)
// 启动带上下文的握手
if err := tlsConn.HandshakeContext(ctx); err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
}
逻辑分析:先通过
Dialer.DialContext建立底层 TCP 连接(受ctx超时/取消约束),再显式调用HandshakeContext确保 TLS 握手阶段同样可中断;若任一环节失败,自动清理已建 TCP 连接,避免资源滞留。
关键参数说明
| 参数 | 作用 | 生产建议 |
|---|---|---|
ctx |
控制整个 TLS 连接生命周期(DNS解析、TCP建连、TLS握手) | 使用 context.WithTimeout(ctx, 5*time.Second) |
config.InsecureSkipVerify |
跳过证书校验 | 仅测试启用,生产必须校验 CA 链 |
错误传播路径(mermaid)
graph TD
A[ContextualTLSDial] --> B[DialContext]
B --> C{Success?}
C -->|Yes| D[HandshakeContext]
C -->|No| E[Return error]
D --> F{Handshake OK?}
F -->|No| G[Close conn & return error]
F -->|Yes| H[Return tls.Conn]
4.2 自定义http.RoundTripper拦截Handshake阶段并注入超时控制的中间件模式
在 TLS 握手阶段注入超时控制,需在 RoundTrip 调用前介入底层 net.Conn 的建立流程。
核心拦截点
- 替换默认
Transport.DialContext - 包装
tls.ClientConn的Handshake方法 - 使用
context.WithTimeout约束握手生命周期
自定义 RoundTripper 示例
type TimeoutRoundTripper struct {
base http.RoundTripper
handshakeTimeout time.Duration
}
func (t *TimeoutRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
ctx, cancel := context.WithTimeout(req.Context(), t.handshakeTimeout)
defer cancel()
// 派生新请求上下文,影响 TLS 握手与连接建立
req = req.Clone(ctx)
return t.base.RoundTrip(req)
}
此实现通过上下文透传超时信号至
crypto/tls底层(如tls.Dialer.DialContext),无需修改 TLS 源码。handshakeTimeout直接约束ClientHandshake()阻塞时长,避免因网络抖动导致无限等待。
| 超时类型 | 作用域 | 是否由本中间件控制 |
|---|---|---|
| DNS 解析 | DialContext 前 |
否 |
| TCP 连接 | DialContext 内 |
是(间接) |
| TLS Handshake | conn.Handshake() |
是(直接) |
| HTTP 请求体传输 | RoundTrip 返回后 |
否 |
graph TD
A[http.RoundTrip] --> B[Context.WithTimeout]
B --> C[DialContext with timeout]
C --> D[tls.ClientConn.Handshake]
D -->|超时触发| E[cancel + EOF error]
4.3 使用golang.org/x/net/http2自定义TLS配置规避默认Transport缺陷的降级策略
Go 标准库 net/http 的默认 http.Transport 在 HTTP/2 场景下对 TLS 配置存在隐式约束:它会强制启用 ALPN 协议协商(h2),且忽略用户对 TLSConfig.NextProtos 的显式设置,导致与某些严格校验 NextProtos 的服务端握手失败。
问题根源
- 默认 Transport 自动注入
[]string{"h2", "http/1.1"},无法清除或替换; - 若服务端仅接受
["h2"]且拒绝含http/1.1的 ALPN 列表,连接将被重置。
解决路径:显式接管 HTTP/2 拆分逻辑
import "golang.org/x/net/http2"
// 创建独立的 http2.Transport,绕过 net/http 默认逻辑
h2Transport := &http2.Transport{
TLSClientConfig: &tls.Config{
NextProtos: []string{"h2"}, // 精确控制,无冗余协议
ServerName: "api.example.com",
},
}
此代码显式构造
http2.Transport实例,直接赋值NextProtos。关键在于:golang.org/x/net/http2不覆盖用户设置,而标准库http.Transport会在configureTransport中强制合并协议列表。
降级策略对比
| 策略 | ALPN 控制力 | 兼容性风险 | 是否需手动注册 |
|---|---|---|---|
标准 http.Transport |
❌(只读注入) | 高(ALPN 不匹配) | 否 |
http2.Transport + 自定义 TLS |
✅(完全可控) | 低(精准匹配) | 是(需 http2.ConfigureTransport) |
graph TD
A[发起 HTTP/2 请求] --> B{使用标准 Transport?}
B -->|是| C[自动注入 h2+http/1.1 → 可能被服务端拒绝]
B -->|否| D[使用 x/net/http2.Transport]
D --> E[NextProtos 由开发者精确指定]
E --> F[握手成功,稳定复用连接]
4.4 静态代码扫描规则:基于go/ast识别未显式设置tls.Config.HandshakeTimeout的高危代码模式
为什么 HandshakeTimeout 缺失构成高危风险
TLS 握手若无限期等待,易被恶意客户端触发资源耗尽(如 Slow TLS 攻击),默认值为 0(禁用超时),等同于无防护。
AST 模式匹配核心逻辑
扫描 &tls.Config{...} 字面量节点,检查字段列表是否完全不含 HandshakeTimeout 标识符:
// 示例:存在风险的代码片段
cfg := &tls.Config{
Certificates: certs,
MinVersion: tls.VersionTLS12,
}
▶️ go/ast 解析后,该结构体字面量含 2 个 *ast.KeyValueExpr,键分别为 "Certificates" 和 "MinVersion",无 "HandshakeTimeout" —— 触发告警。
检测覆盖场景对比
| 场景 | 是否触发告警 | 原因 |
|---|---|---|
&tls.Config{HandshakeTimeout: 10 * time.Second} |
否 | 显式赋值 |
cfg := new(tls.Config) + 后续字段赋值 |
是 | AST 字面量中不可见字段初始化 |
使用 tls.Config{} 零值构造 |
是 | 字段列表为空 |
修复建议
- ✅ 强制设置
HandshakeTimeout: 5 * time.Second - ✅ 或封装安全构造函数统一注入默认超时
第五章:从TLS握手泄漏到云原生连接治理范式的升维思考
在2023年某大型金融云平台的红队演练中,攻击者通过被动监听边缘网关(Envoy 1.24)与上游服务间的TLS 1.2握手流量,成功提取出Server Name Indication(SNI)字段中的内部服务域名 payment-core-staging.internal.bankcloud,并结合证书有效期(仅72小时)与ALPN协议协商结果(h2),精准识别出该链路为gRPC灰度通道。这一看似微小的元数据泄漏,直接导致后续横向移动成功率提升300%。
TLS握手阶段的隐式信令风险
现代云原生架构中,TLS握手已不仅是加密协商过程,更成为服务发现、路由策略、熔断决策的关键信令源。例如,当Istio 1.21的Sidecar代理在ISTIO_MUTUAL模式下发起握手时,其ClientHello中携带的supported_groups扩展(如x25519)会暴露客户端支持的密钥交换能力,而服务端响应的key_share则泄露其实际选用的密钥参数——这些信息被恶意爬虫用于构建服务指纹图谱。
连接治理的三层抽象模型
| 抽象层级 | 治理焦点 | 典型工具链 | 风险案例 |
|---|---|---|---|
| 网络层 | IP/端口连通性 | Cilium eBPF | NodePort暴露导致kube-proxy规则绕过 |
| 协议层 | TLS版本、SNI、ALPN | OpenSSL 3.0+ TLS 1.3 Early Data | SNI明文传输致多租户域名泄露 |
| 语义层 | gRPC方法名、HTTP路径前缀 | OpenTelemetry Collector | /v1/transfer路径被误判为支付核心而非测试接口 |
基于eBPF的握手元数据净化实践
某电商中台团队在Kubernetes v1.26集群中部署定制eBPF程序,在tcp_sendmsg钩子点拦截TLS ClientHello报文,对SNI字段执行动态混淆:
// bpf_prog.c 片段
SEC("socket/sendmsg")
int socket_filter(struct __sk_buff *skb) {
if (is_tls_handshake(skb)) {
char *sni = parse_sni_from_client_hello(skb);
if (sni && is_internal_service(sni)) {
// 替换为统一占位符,保留长度不变防协议解析异常
bpf_skb_store_bytes(skb, sni_offset, "svc-xxxxxx.internal", 18, 0);
}
}
return 0;
}
该方案使SNI泄漏率从100%降至0%,且未增加TLS握手延迟(实测P99
服务网格的连接生命周期重构
在阿里云ACK Pro集群中,将传统“连接即资源”的治理模式升级为“连接即策略实例”。每个mTLS连接建立时,Envoy Proxy通过xDS API动态加载对应服务的ConnectionPolicy对象,其中包含:
handshake_timeout: 3smax_idle_duration: 300stls_fingerprint_rules: [ { hash: "sha256:...", action: "deny" } ]semantic_routing: { grpc_method: "^/bank.*Transfer$", route_to: "payment-canary" }
此机制使某次因OpenSSL CVE-2023-0286引发的证书吊销事件中,受影响连接自动降级至TLS 1.2并启用备用证书链,业务无感切换。
跨云环境的连接一致性验证
采用Mermaid流程图描述多云连接治理校验逻辑:
flowchart LR
A[源集群Ingress] -->|TLS 1.3握手| B[AWS EKS服务]
A -->|SNI重写| C[GCP GKE服务]
B --> D{连接策略引擎}
C --> D
D -->|匹配规则集| E[统一审计日志]
D -->|不一致告警| F[自动触发PolicySync Job]
F --> G[同步xDS配置至所有控制平面]
某跨国零售企业通过该机制,在混合云架构中实现98.7%的连接策略收敛率,跨云TLS握手失败率下降至0.02%。
