Client VPN 把地址分配给客户端,握手也完成了,连接却仍然哪里都去不了。在动手检查证书或客户端版本之前,先看终端节点自己的路由表。AWS Client VPN 终端节点只有在某条路由覆盖目标前缀时才转发数据包;若没有任何路由匹配,数据包会在终端节点被丢弃,目标主机根本看不到 SYN。

这是转发决策,不是认证决策。它发生在隧道建立之后、授权规则评估之后,所以证书有效、组也有效,连接依然可能是死的。

终端节点只转发其路由表覆盖的流量

每条路由是一个目标 CIDR 加上应当接收流量的子网。终端节点把客户端的目标与此列表对照,选出匹配项,再把数据包送入关联的子网。 没有条目就没有下一跳。

注意路由不是什么。它不是 VPC 路由表条目,也不授予任何权限。授权规则决定某个组能否到达目标;路由决定终端节点有没有地方可送。两者必须一致,而它们失败时的症状不同,所以猜哪一个坏了会白白耗掉一个下午。

典型的错配是对等 VPC。有人为 10.20.0.0/24 加了路由,但主机实际位于 10.20.4.0/24,地址落在所有条目之外。客户端抓包会看到反复出现 SYN 却没有任何应答,目标侧日志则什么都没有。

先读路由列表,再查证书

终端节点的路由是独立对象,所以直接查询它们,而不是去读 VPC 的路由表:

aws ec2 describe-client-vpn-routes --client-vpn-endpoint-id cvpn-endpoint-0123456789abcdef0 --query 'Routes[].{Dest:DestinationCidr,Target:TargetSubnet,Origin:Origin}'

从失败客户端取出确切地址和前缀长度,然后逐字符核对覆盖情况。最长前缀匹配生效,所以当更窄的路由指向别处时,更宽的路由救不了你。

用 for_each 为每个目标建一个资源

手写的路由块会在有人往控制台添加前缀的瞬间漂移。把目标放进映射,每个键建一个资源,这样每次增删都保持隔离。

variable "client_vpn_routes" {
  type = map(string)
  default = {
    "10.20.0.0/16"  = "subnet-0aa11bb22cc33dd44"
    "172.31.0.0/20" = "subnet-0aa11bb22cc33dd44"
  }
}

resource "aws_ec2_client_vpn_route" "this" {
  for_each               = var.client_vpn_routes
  client_vpn_endpoint_id = var.client_vpn_endpoint_id
  destination_cidr_block = each.key
  target_vpc_subnet_id   = each.value
  depends_on             = [aws_ec2_client_vpn_network_association.this]
}

保留对网络关联的依赖。路由需要关联的子网,所以同时创建两者的 apply 必须先排序关联;没有那条引用,Terraform 可以先去尝试路由。然后读计划:新增路由的数量应当与你添加的前缀数量一致。如果计划还要替换终端节点,说明终端节点资源写错了,应用它会断开所有已连接的客户端。

接管并非自己创建的终端节点

多数团队继承的是在控制台里建好的终端节点,所以根本没有路由代码。不要仅仅为了把路由纳入 Terraform 就再建一个终端节点。

改用导入,或者把 id 作为变量传入,让继承来的对象保持不受管理。导入需要提供商为每种资源类型规定的标识符格式,它编码了终端节点、目标前缀和目标子网。请从资源文档里复制该字符串,而不是凭记忆拼出来,跑一次计划,并在新增任何东西之前确认没有变更。

按数据包实际经过的顺序去追踪

当路由存在而连接仍然失败时,按顺序走一遍路径,而不是在顶层猜。

  1. 路由:前缀覆盖目标,并指向与终端节点关联的子网。
  2. 授权:某条授权规则允许该用户组访问这个目标。只有路由永远不会放行。
  3. 回程:目标子网的路由表把应答送回终端节点,安全组允许来自客户端地址池的端口。
  4. 应用:从客户端发起真实的 TCP 连接,然后确认目标日志显示的是来自地址池的预期源地址。

每次 apply 只改一个前缀,随后重读路由列表,并把目标、授权规则和回程路径一起记录下来。下一位排查被拒连接的人应当从那份清单开始,而不是从证书理论开始。

相关文章