各位技术同仁,下午好!
今天,我们将深入探讨一个在现代分布式系统安全领域至关重要的概念——Zero-Trust Proxy Architecture。尤其,我们将聚焦于如何利用Go语言,来构建一个强大且高效的、基于身份验证(Identity-based)的微服务流量网关。这不仅仅是一个理论探讨,更是一次实践指南,旨在帮助大家理解并掌握在微服务架构中实现零信任安全的关键技术。
在云计算、容器化和微服务盛行的今天,传统的“城堡与护城河”式安全模型已显得力不合时宜。内部网络不再是天然可信的堡垒,每一个请求、每一次交互都必须经过严格的验证。这正是零信任(Zero Trust)理念的核心——“永不信任,始终验证”(Never Trust, Always Verify)。而将这一理念落地到微服务架构中,一个智能的、身份驱动的流量代理,便是不可或缺的组件。
零信任的崛起:从边界防御到无边界验证
传统安全模型的局限性
在过去几十年里,企业网络安全主要依赖于边界防御。防火墙、入侵检测系统(IDS)、入侵防御系统(IPS)构成了网络的“护城河”,将外部不可信的流量与内部可信的流量隔离开来。一旦攻击者突破了这层边界,便可能在内部网络中横行无阻,因为内部资源之间默认是相互信任的。
这种模型在以下几个方面暴露了其根本性缺陷:
- 内部威胁: 恶意内部人员或被攻陷的内部系统,可以直接绕过外部防御。
- 云与移动化: 随着业务迁移到云端,员工远程办公,以及移动设备的普及,传统的网络边界日益模糊,甚至消失。
- 微服务与API: 微服务架构打破了传统的单体应用,服务间通过API进行大量、频繁的通信。每个服务都可能成为攻击目标,而服务间的信任关系必须重新定义。
- 供应链攻击: 第三方组件或服务可能成为新的攻击入口,即使自身防御完善,也可能因依赖关系而受损。
零信任的核心原则
为了应对这些挑战,零信任模型应运而生。它不是一个单一的技术,而是一种安全理念和架构方法,其核心原则可以概括为:
- 显式验证 (Verify Explicitly): 无论用户、设备、应用还是数据,在每次访问资源时都必须经过严格的身份验证和授权。这包括验证身份、设备状态、位置、服务类型、数据敏感度等所有可用上下文。
- 最小权限访问 (Least Privilege Access): 授予用户或服务完成其任务所需的最小权限,并定期审查和撤销不必要的权限。权限不应是永久的,而应是按需的、有时限的。
- 假设泄露 (Assume Breach): 始终假设系统已经或可能被攻破。这意味着安全策略的设计不应寄希望于完全阻止攻击,而应侧重于如何快速检测、响应和限制攻击的影响。所有通信都应加密,所有数据都应受保护。
下表对比了传统安全模型与零信任模型的主要区别:
| 特征 | 传统安全模型 | 零信任模型 |
|---|---|---|
| 核心理念 | 默认信任内部,怀疑外部 | 永不信任,始终验证 |
| 安全边界 | 明确的网络边界(护城河) | 无明确边界,每个资源都是自己的边界 |
| 访问控制 | 基于网络位置和IP | 基于身份、上下文和策略 |
| 内部流量 | 默认可信 | 默认不可信,需验证 |
| 威胁模型 | 外部攻击为主 | 内部和外部攻击同等重视 |
| 加密 | 主要在边界,内部可选 | 端到端加密为默认 |
| 监控 | 边界流量监控 | 持续监控所有流量和访问行为 |
| 权限管理 | 宽泛权限,持久授予 | 最小权限,按需授予,动态调整 |
零信任代理架构的定位
在微服务环境中,零信任代理架构正是实现“显式验证”和“最小权限访问”的关键执行点。它作为一个智能的流量网关,部署在客户端与微服务之间,或者微服务与微服务之间,拦截所有请求,并强制执行安全策略。这个代理并非传统意义上的API网关,它更专注于安全验证,并且是身份驱动的。
零信任代理架构的核心组件与功能
一个零信任代理,其核心职责是在每次请求到达实际的微服务之前,对其进行全面的安全审查。为了实现这一目标,它需要具备以下核心组件和功能:
- 身份验证 (Authentication):
- 用户身份: 验证请求发起者的用户身份,通常通过JWT (JSON Web Token) 或其他基于令牌的机制。代理需要能够解析、验证这些令牌的签名、过期时间、签发者等。
- 服务身份: 验证请求发起者的服务身份,尤其在服务间通信时。这通常通过相互TLS (mTLS) 或基于SPIFFE/SPIRE等服务身份框架实现。
- 授权 (Authorization):
- 根据已验证的身份、请求的资源、操作类型以及其他上下文信息(如设备状态、地理位置、时间等),判断该请求是否被允许。
- 这通常涉及一个策略决策点 (Policy Decision Point, PDP),代理作为策略执行点 (Policy Enforcement Point, PEP)。
- 可以实现基于角色的访问控制 (RBAC) 或基于属性的访问控制 (ABAC)。
- 策略执行 (Policy Enforcement):
- 根据身份验证和授权的结果,代理决定是允许请求继续,还是拒绝并返回错误。
- 策略可以是静态配置的,也可以是动态从外部策略引擎(如Open Policy Agent, OPA)获取的。
- 流量路由 (Traffic Routing):
- 将通过安全检查的请求转发到正确的后端微服务实例。这可能涉及服务发现、负载均衡等功能。
- 加密与TLS终止 (Encryption & TLS Termination):
- 作为入口点,代理通常会终止客户端的TLS连接,并与后端服务建立新的TLS连接(或mTLS),确保端到端加密。
- 审计与日志 (Auditing & Logging):
- 记录所有请求的详细信息,包括身份、授权结果、请求路径、时间、源IP等,以便进行安全审计、故障排查和合规性审查。
- 其他安全功能 (Optional Security Features):
- 限速 (Rate Limiting): 防止DDoS攻击或滥用。
- 熔断 (Circuit Breaking): 保护后端服务免受级联故障影响。
- 请求/响应转换 (Request/Response Transformation): 修改请求头、正文等以适应后端服务。
- 敏感数据过滤 (Sensitive Data Filtering): 在日志或转发前移除敏感信息。
代理的部署位置
零信任代理可以以多种方式部署:
- 集中式网关 (Centralized Gateway): 作为所有外部流量进入微服务集群的统一入口点。本讲座将主要聚焦于这种模式。
- 服务网格 Sidecar (Service Mesh Sidecar): 在服务网格中,每个微服务实例旁边部署一个代理(通常称为Sidecar),负责该服务的入站和出站流量。Istio、Linkerd等是典型的服务网格实现。这种模式更适用于服务间的内部通信,而集中式网关更适用于外部流量的入口。
集中式网关的优势在于统一管理和集中策略执行,但在大规模集群中可能会成为性能瓶颈或单点故障。Sidecar模式则将代理能力下放到每个服务,提高了弹性,但增加了部署和管理的复杂性。
身份驱动安全:零信任的基石
在零信任架构中,“身份”是所有安全决策的起点。无论是人还是机器,都必须拥有一个可验证的身份。
用户身份:JWT与OAuth2/OIDC
对于用户(如最终用户或前端应用)的身份验证,业界标准通常是结合OAuth2和OpenID Connect (OIDC) 协议,使用JWT作为身份凭证。
- OAuth2: 一个授权框架,允许第三方应用代表用户访问受保护资源,而无需知晓用户凭据。它定义了授权流程,但本身不处理身份验证。
- OpenID Connect (OIDC): 构建在OAuth2之上的身份层,提供了标准的身份验证机制。它引入了ID Token(一个JWT),用于携带用户的身份信息。
JWT (JSON Web Token) 是一种紧凑且自包含的方式,用于在各方之间安全地传输信息。它通常由三部分组成,用点分隔:
- Header (头部): 通常包含令牌的类型(JWT)和所使用的签名算法(如HS256或RS256)。
{ "alg": "RS256", "typ": "JWT" } - Payload (载荷): 包含实际的声明(Claims)。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明:
- Registered Claims (注册声明): 预定义的一些声明,推荐使用但并非强制,如
iss(issuer, 签发者),exp(expiration time, 过期时间),sub(subject, 主题),aud(audience, 接收者) 等。 - Public Claims (公共声明): 可以自行定义,但为了避免冲突,应在IANA JSON Web Token Registry中注册,或使用包含冲突避免命名空间的URI。
- Private Claims (私有声明): 由发送方和接收方约定好的自定义声明。
{ "sub": "user123", "name": "John Doe", "admin": true, "iss": "https://auth.example.com", "aud": "my-microservice-gateway", "exp": 1678886400, // 过期时间戳 "roles": ["admin", "editor"] }
- Registered Claims (注册声明): 预定义的一些声明,推荐使用但并非强制,如
- Signature (签名): 用于验证令牌在传输过程中没有被篡改,并验证其签发者。签名是使用Header中指定的算法,对编码后的Header、编码后的Payload和密钥(或私钥)进行加密计算得出的。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
代理在接收到JWT后,需要:
- 验证签名: 使用公钥(如果JWT由RS256等非对称算法签名)或共享密钥(如果由HS256等对称算法签名)来验证JWT的签名。这是防止篡改的关键。
- 验证声明: 检查
exp(过期时间)、iss(签发者)、aud(接收者)等声明,确保令牌有效且适用于当前服务。 - 提取身份信息: 从Payload中提取用户ID、角色、权限等信息,存储在请求上下文中供后续授权决策使用。
服务身份:mTLS与SPIFFE/SPIRE
对于服务(微服务之间)的身份验证,mTLS (Mutual TLS) 是黄金标准。它要求客户端和服务端在建立TLS连接时,除了服务端向客户端出示证书外,客户端也必须向服务端出示证书。双方都验证对方的证书,从而实现双向身份验证。
- mTLS工作流程简述:
- 客户端发起TLS握手。
- 服务端发送其证书给客户端。
- 客户端验证服务端证书的有效性。
- 服务端请求客户端证书。
- 客户端发送其证书给服务端。
- 服务端验证客户端证书的有效性(包括信任链、过期时间、撤销状态等)。
- 双方完成TLS握手,建立加密连接。
通过mTLS,代理可以验证发起请求的微服务的身份,即哪个服务在尝试访问。结合服务网格,mTLS的证书管理和轮换可以自动化。
SPIFFE (Secure Production Identity Framework for Everyone) 和 SPIRE (SPIFFE Runtime Environment) 项目进一步提升了服务身份管理的自动化和可移植性。它们为每个工作负载(如Pod、容器)提供了一个唯一的、可验证的短期加密身份(X.509 SVID或JWT SVID),并自动分发和轮换这些身份。代理可以集成SPIFFE/SPIRE来验证服务身份。
利用Go构建基于身份验证的微服务流量网关
Go语言凭借其强大的并发原语、高性能的网络库、简洁的语法以及丰富的生态系统,成为构建高性能、高并发网络服务(如API网关、代理)的理想选择。
我们将逐步构建一个简化的零信任代理,重点关注身份验证、授权和流量转发。
1. 基础HTTP服务器与反向代理
首先,我们从一个最简单的Go HTTP服务器开始,并集成net/http/httputil包提供的ReverseProxy功能。
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
// TargetService 定义后端微服务
type TargetService struct {
Name string
BaseURL *url.URL
}
func main() {
// 假设我们有一个后端服务运行在 localhost:8081
backendURL, err := url.Parse("http://localhost:8081")
if err != nil {
log.Fatalf("Failed to parse backend URL: %v", err)
}
target := &TargetService{
Name: "example-service",
BaseURL: backendURL,
}
// 创建一个反向代理处理器
proxy := httputil.NewSingleHostReverseProxy(target.BaseURL)
// 定义代理服务器的处理器函数
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Incoming request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
// 这里可以添加一些预处理逻辑,例如修改请求头
r.Host = target.BaseURL.Host // 确保后端服务能正确识别Host
proxy.ServeHTTP(w, r)
})
log.Printf("Starting Zero-Trust Proxy on :8080, forwarding to %s", target.BaseURL)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Proxy server failed: %v", err)
}
}
// 模拟一个简单的后端服务,用于测试
// 可以在单独的Go文件中运行,或者直接在main函数中启动一个goroutine
/*
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Backend received request: %s %s", r.Method, r.URL.Path)
fmt.Fprintf(w, "Hello from backend service! Path: %sn", r.URL.Path)
fmt.Fprintf(w, "Request Headers: %vn", r.Header)
fmt.Fprintf(w, "Timestamp: %sn", time.Now().Format(time.RFC3339))
})
log.Println("Starting backend service on :8081")
if err := http.ListenAndServe(":8081", nil); err != nil {
log.Fatalf("Backend server failed: %v", err)
}
}
*/
这段代码展示了Go中构建反向代理的基本骨架。httputil.NewSingleHostReverseProxy创建了一个将所有请求转发到指定URL的代理。在http.HandleFunc中,我们可以在请求转发前添加自定义逻辑。
2. JWT身份验证中间件
现在,我们将引入JWT身份验证。我们需要一个JWT库,例如github.com/golang-jwt/jwt/v5。
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 定义一个自定义的Claims结构体,用于解析JWT
type MyClaims struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
// UserIdentity 结构体用于存储从JWT中提取的用户身份信息
type UserIdentity struct {
UserID string
Roles []string
// 更多身份信息可以添加
}
// contextKey 类型用于在context中存储和获取值
type contextKey string
const (
// UserIdentityContextKey 是存储用户身份的context键
UserIdentityContextKey contextKey = "userIdentity"
)
// JWTVerifier 接口定义了JWT验证的行为
type JWTVerifier interface {
Verify(tokenString string) (*UserIdentity, error)
}
// RS256JWTVerifier 实现了JWTVerifier接口,使用RS256算法验证JWT
type RS256JWTVerifier struct {
PublicKey interface{} // RSA公钥
Audience string // 预期接收者
Issuer string // 预期签发者
}
// NewRS256JWTVerifier 创建一个RS256JWTVerifier实例
func NewRS256JWTVerifier(publicKeyPem []byte, audience, issuer string) (*RS256JWTVerifier, error) {
publicKey, err := jwt.ParseRSAPublicKeyFromPEM(publicKeyPem)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %w", err)
}
return &RS256JWTVerifier{
PublicKey: publicKey,
Audience: audience,
Issuer: issuer,
}, nil
}
// Verify 验证JWT令牌
func (v *RS256JWTVerifier) Verify(tokenString string) (*UserIdentity, error) {
token, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法是否是RS256
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return v.PublicKey, nil
}, jwt.WithAudience(v.Audience), jwt.WithIssuer(v.Issuer), jwt.WithLeeway(5*time.Second)) // 允许5秒的时钟偏差
if err != nil {
return nil, fmt.Errorf("invalid JWT token: %w", err)
}
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
return &UserIdentity{
UserID: claims.UserID,
Roles: claims.Roles,
}, nil
}
return nil, fmt.Errorf("invalid or expired JWT claims")
}
// AuthMiddleware 是一个HTTP中间件,用于验证JWT
func AuthMiddleware(verifier JWTVerifier, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized)
return
}
tokenString := parts[1]
identity, err := verifier.Verify(tokenString)
if err != nil {
log.Printf("JWT verification failed: %v", err)
http.Error(w, "Unauthorized: Invalid or expired token", http.StatusUnauthorized)
return
}
// 将用户身份存储在请求的context中,以便后续处理程序访问
ctx := context.WithValue(r.Context(), UserIdentityContextKey, identity)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
在AuthMiddleware中:
- 我们从请求头
Authorization: Bearer <token>中提取JWT。 JWTVerifier接口定义了验证JWT的方法,RS256JWTVerifier是其一个实现,用于使用RSA公钥验证RS256签名的JWT。- 如果验证成功,我们将
UserIdentity结构体存储在请求的context.Context中。这是Go中传递请求范围值(如用户身份、trace ID等)的标准做法。 - 如果验证失败,则返回
401 Unauthorized错误。
要使用这个中间件,我们需要在main函数中进行如下修改:
// main函数修改
func main() {
// ... (原有的backendURL和target定义) ...
// 模拟一个RSA公钥PEM格式字符串
// !!! 注意:在生产环境中,这应该从安全配置管理系统(如Vault)或JWKS端点动态获取
// 这是一个假的公钥,您需要替换为实际的公钥
// 生成一个RSA密钥对:
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
// 然后将public.pem内容复制到这里
fakePublicKeyPEM := []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyY0sFmD91tXlP7wP91v1
... 您的公钥内容 ...
W0Xf0xT/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9f/v9