# The relay: one small CVM in Tencent Cloud that terminates TLS for the .cn # domain and forwards over WireGuard to the homelab ingress. # # Why a relay and not Cloudflare: mainland clients reach a Tencent edge far # faster than a Cloudflare edge, and Cloudflare's China network is an # Enterprise product. The homelab keeps zero inbound exposure because the # tunnel is dialled outbound from the cluster. # # ICP FILING: a mainland resource serving HTTP/HTTPS for a domain requires an # ICP filing naming that domain. Without it this instance accepts the # connection and the mainland network resets the HTTP layer. Put the relay in a # non-mainland region (ap-hongkong, ap-singapore) when no filing exists. resource "tencentcloud_vpc" "edge" { name = "${replace(var.china_domain, ".", "_")}_edge" cidr_block = var.edge_vpc_cidr } resource "tencentcloud_subnet" "edge" { name = "${replace(var.china_domain, ".", "_")}_edge" vpc_id = tencentcloud_vpc.edge.id availability_zone = var.tencent_availability_zone cidr_block = var.edge_subnet_cidr } resource "tencentcloud_key_pair" "edge" { key_name = "${replace(var.china_domain, ".", "_")}_edge" public_key = var.relay_ssh_public_key } resource "tencentcloud_security_group" "edge" { name = "${replace(var.china_domain, ".", "_")}_edge" description = "TLS + WireGuard for the ${var.china_domain} relay; SSH from the operator only" } resource "tencentcloud_security_group_rule" "ssh" { security_group_id = tencentcloud_security_group.edge.id type = "ingress" ip_protocol = "tcp" port_range = "22" cidr_ip = var.relay_admin_cidr policy = "accept" } resource "tencentcloud_security_group_rule" "tls" { security_group_id = tencentcloud_security_group.edge.id type = "ingress" ip_protocol = "tcp" port_range = "80,443" cidr_ip = "0.0.0.0/0" policy = "accept" } resource "tencentcloud_security_group_rule" "wireguard" { security_group_id = tencentcloud_security_group.edge.id type = "ingress" ip_protocol = "udp" port_range = tostring(var.wireguard_port) cidr_ip = var.relay_admin_cidr policy = "accept" } # cloud-init: WireGuard listener, forwarding sysctls, and Caddy for TLS. # The relay never holds application credentials; it forwards bytes. # # NOTE ON THE PRIVATE KEY: user_data is visible in the instance metadata and in # Terraform state. That is a known limitation of bootstrapping WireGuard this # way. The argument is wrapped in sensitive() to keep it out of plan/apply # output, but anyone with console or state access can read it. Rotate the key # if the state or the instance metadata is ever exposed. locals { china_fqdns = [for host in var.china_hostnames : "${host}.${var.china_domain}"] caddyfile = templatefile("${path.module}/templates/Caddyfile.tpl", { acme_email = var.acme_email hosts = join(", ", local.china_fqdns) ingress_upstream = var.ingress_upstream }) relay_user_data = <<-EOT #cloud-config package_update: true package_upgrade: false manage_etc_hosts: true timezone: Asia/Shanghai packages: [wireguard, wireguard-tools, caddy] write_files: - path: /etc/wireguard/wg0.conf permissions: "0600" content: | [Interface] Address = ${var.wireguard_relay_address} ListenPort = ${var.wireguard_port} PrivateKey = ${var.wireguard_relay_private_key} # Masquerade forwarded traffic so the homelab peer only has to accept # packets sourced from the relay's tunnel address. Without this the # homelab peer's AllowedIPs would drop the original client source IP. PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE [Peer] PublicKey = ${var.wireguard_homelab_public_key} AllowedIPs = ${var.wireguard_homelab_allowed_ips} PersistentKeepalive = 25 - path: /etc/sysctl.d/99-wireguard.conf content: | net.ipv4.ip_forward = 1 - path: /etc/caddy/Caddyfile permissions: "0644" content: ${jsonencode(local.caddyfile)} runcmd: - sysctl --system - systemctl enable --now wg-quick@wg0 - caddy validate --config /etc/caddy/Caddyfile - systemctl enable --now caddy - systemctl restart caddy EOT } resource "tencentcloud_instance" "relay" { instance_name = "${replace(var.china_domain, ".", "_")}_edge" availability_zone = var.tencent_availability_zone image_id = var.relay_image_id instance_type = var.relay_instance_type vpc_id = tencentcloud_vpc.edge.id subnet_id = tencentcloud_subnet.edge.id security_groups = [tencentcloud_security_group.edge.id] key_ids = [tencentcloud_key_pair.edge.id] allocate_public_ip = true # Outbound only needs a few Mbps; inbound TLS is the same order. internet_max_bandwidth_out = var.relay_internet_max_bandwidth_out system_disk_type = var.relay_system_disk_type system_disk_size = var.relay_system_disk_size # The private key is embedded in this bootstrap script. sensitive() keeps it # out of CLI output; it does not remove it from state or instance metadata. user_data = sensitive(local.relay_user_data) tags = { role = "china-edge" domain = var.china_domain } # Accidental `terraform destroy` would take the only China-facing edge down. # Remove this block deliberately when a replacement path exists. lifecycle { prevent_destroy = true } } # DNS for the .cn domain lives at DNSPod (Tencent). Each subdomain resolves to # the relay's public address, and the relay proxies to the homelab over the # tunnel. The "中国境内" line is the default line; no separate line records. resource "tencentcloud_dnspod_record" "china" { for_each = toset(var.china_hostnames) domain = var.china_domain sub_domain = each.value record_type = "A" record_line = var.china_dns_record_line value = tencentcloud_instance.relay.public_ip ttl = var.china_dns_ttl remark = "China edge relay; origin is the homelab ingress over WireGuard" }