VPC内网互通:让被黑洞的服务器仍能访问

事情起因很简单,前段时间我的华为云服务器又被闲的蛋疼的人刷了DDoS礼物,也是成功地触发了EIP(弹性公网IP)黑洞封堵。
喜报如下:

尊敬的(Rin):
感谢您使用华为云产品,非常抱歉打扰您。
在北京时间2025/09/23 10:26,华为云监测到您的弹性公网IP(EIP)实例 发生EIP封堵事件。 详情:
节点:亚太-新加坡
实例名称:119.8.185.128
实例ID:b002926d-60de-4951-a9e9-b337e3a550d8
告警描述:{"reason_zh": 尊敬的nekoworks:华为云DDoS防护服务在2025-09-23 10:26:56检测到您的EIP 119.8.185.128 异常流量已超过DDoS防护峰值触发封堵,封堵时带宽为 28 Gbps, 您服务器的访问将受到限制,24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力,保障服务器稳定运行。, "reason_en": Dear nekoworks: HUAWEI CLOUD Anti-DDoS Service detects that your EIP 119.8.185.128 abnormal traffic has exceeded the DDoS protection peak at 2025-09-23 10:26:56 and triggers blocking. Bandwidth when blocking is 28 Gbps, Access to your server will be restricted, and the system will automatically unblock it for you after 24 hours. You can log in to the official website of HUAWEI CLOUD to purchase Advanced Anti-DDoS service to enhance protection capabilities and ensure stable server operation.}
原因分析:尊敬的nekoworks:华为云DDoS防护服务在2025-09-23 10:26:56检测到您的EIP 119.8.185.128 异常流量已超过DDoS防护峰值触发封堵,封堵时带宽为 28 Gbps您服务器的访问将受到限制,24小时后系统将自动为您解封。您可登陆官网购买DDoS高防服务提升防护能力,保障服务器稳定运行。
如有疑问,您可以登录华为云官网提交工单咨询,会有相应工程师为您核实查看。

IP我就懒得打码了,反正就是这个网站域名平时解析的IP,有什么好打码的,ping一下就能看到了。

我在之前的文章,就有说过一套抗DDoS的方案,并且已经付诸实践了(详见文章:《当网站遭遇DDoS:如何优雅地应对云服务器被封堵》)。很显然,并没有太大影响,在网站主服务器被打黑洞的三分钟后,脚本就自动切换服务器到备用服务器并且套上了Cloudflare的代理,无需我干预,唯一的不足就是在被DDoS期间网站访问变慢了。
但是,当时我就有说过,那套方案是有缺陷的,也就是因为网站数据同步是单向的,在被攻击期间交互产生的数据,后面会被覆盖掉。
好巧不巧,华为云又送钱了,我寻思着我之前开的华为云服务器已经续费十年多了,不如开个新的云服务器,搞VPC内网互通,彻底解决上述的痛点问题。

这里说个题外话,我发现新开的华为云 Flexus L 实例相比于之前已经大幅缩水,网络回国优化没了,而且CPU性能只有原来的三分之一了,已经变得跟阿里云的轻量服务器一个水平了,而且价格还更高。除非你和我一样是白嫖的,否则真金白银买我是不推荐,因为差不多一样的东西,阿里云那边要大概便宜个10%-20%,买华为云真不划算了。现在华为云的 Flexus L 实例算是把华为手机的高价低配学过来了,难绷。
但是老实例性能和网络那还是相当不错的。

资源准备

为了实现这个方案,你需要准备两台云服务器,并且两台服务器必须在同一个地域,以便进行VPC内网互通。虽然同一个地域就足够,但将两台服务器放在同一个可用区会获得更好的内网延迟(通常<1ms),而跨可用区的内网延迟可能会稍高(但通常也在1-5ms范围内)。这就是最重要的前提要求,日常访问的可以是两台服务器的任意一台,只需要它的性能可以满足业务要求。对于另一台服务器,那是作为网站流量跳板服务器,最低配即可,方便节约成本,因为只需要在上面运行一个代理流量程序,在主实例EIP被封堵时代理接管流量入口。因为本方案抗DDoS采用的是Cloudflare的CDN,所以跳板实例的网络可以差一点,只需要保证到Cloudflare的回源服务器线路通畅即可,国内访问绕路都不是事。平时流量也不会从这边走,只是作为保障用途应急使用。

网络配置

一般情况下,云服务器在同一个地域会默认分配到同一个VPC实现内网互通,如果没有互通,修改两个云服务器的VPC为同一个VPC即可,或者使用VPC对等连接(VPC Peering),详情可以查阅各类云的参考文档,或者直接去发工单骚扰客服。
当其中一个服务器ping另一个服务器的内网IP地址有反应,那就说明内网互通成功了,可以进行下一步工作。
就像这样:
01.png
主服务器:

NyankoHost [~]# ping 192.168.1.255
PING 192.168.1.255 (192.168.1.255): 56 data bytes
64 bytes from 192.168.1.255: seq=0 ttl=64 time=1.152 ms
64 bytes from 192.168.1.255: seq=1 ttl=64 time=0.950 ms
64 bytes from 192.168.1.255: seq=2 ttl=64 time=0.932 ms
64 bytes from 192.168.1.255: seq=3 ttl=64 time=0.879 ms
64 bytes from 192.168.1.255: seq=4 ttl=64 time=0.899 ms
64 bytes from 192.168.1.255: seq=5 ttl=64 time=0.891 ms
64 bytes from 192.168.1.255: seq=6 ttl=64 time=0.885 ms
64 bytes from 192.168.1.255: seq=7 ttl=64 time=0.878 ms
^C
--- 192.168.1.255 ping statistics ---
8 packets transmitted, 8 packets received, 0% packet loss
round-trip min/avg/max = 0.878/0.933/1.152 ms

代理服务器:

MewHost:~# ping 192.168.13.229
PING 192.168.13.229 (192.168.13.229): 56 data bytes
64 bytes from 192.168.13.229: seq=0 ttl=64 time=1.051 ms
64 bytes from 192.168.13.229: seq=1 ttl=64 time=1.032 ms
64 bytes from 192.168.13.229: seq=2 ttl=64 time=1.006 ms
64 bytes from 192.168.13.229: seq=3 ttl=64 time=0.978 ms
64 bytes from 192.168.13.229: seq=4 ttl=64 time=0.988 ms
64 bytes from 192.168.13.229: seq=5 ttl=64 time=0.978 ms
64 bytes from 192.168.13.229: seq=6 ttl=64 time=0.980 ms
64 bytes from 192.168.13.229: seq=7 ttl=64 time=0.999 ms
^C
--- 192.168.13.229 ping statistics ---
8 packets transmitted, 8 packets received, 0% packet loss
round-trip min/avg/max = 0.978/1.001/1.051 ms

设置代理服务器

建议使用一个轻量化的Linux发行版来节省资源,例如Alpine,本教程就是基于Alpine Linux 3.20部署的,然后安装上Nginx。
编辑Nginx的默认站点配置文件/etc/nginx/http.d/default.conf,修改如下:

server {
    listen 80  default_server;
    #listen [::]:443 ssl default_server;

    server_name _;  # 这是一个通配符,表示任何未匹配的主机名

    #return 444;  # 关闭连接,不发送任何响应

    root /var/www/; # 静态文件的位置
    index index.html; # 默认主页

    location / {
        try_files $uri $uri/ =404; # 尝试提供请求的文件或目录,如果不存在则返回404
    }

    # 处理错误页面
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
}

server {
    listen 443 ssl default_server;
    #listen [::]:443 ssl default_server;

    server_name _;  # 这是一个通配符,表示任何未匹配的主机名

    ssl_certificate /etc/nginx/ssl/self_cert.pem;
    ssl_certificate_key /etc/nginx/ssl/self_cert.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384';

    root /var/www/; # 静态文件的位置
    index index.html; # 默认主页

    location / {
        try_files $uri $uri/ =404; # 尝试提供请求的文件或目录,如果不存在则返回404
    }

    # 处理错误页面
    error_page 404 /404.html;
    location = /404.html {
        internal;
    }
}

这里的配置文件是匹配默认站点,对于https则采用自签名证书匹配后返回默认页面,防止被证书扫描暴露备用服务器IP导致一起被打。
至于怎么安装Nginx,怎么启动,怎么生成自签名证书,这些我之前的文章说过很多次了,不会的可以翻一下我的博客文章,例如《实战:帮群友迁移Typecho博客并解决数据库问题》,这里不再赘述。
接下来配置代理的配置文件/etc/nginx/http.d/proxy_nekopara_uk.conf

server {
    listen 443 ssl;
    server_name www.nekopara.uk;

    ssl_certificate /data/certs/nekopara_uk_cf.pem;
    ssl_certificate_key /data/certs/nekopara_uk_cf.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;


    location / {
        client_max_body_size 32m;
        proxy_pass https://www.nekopara.uk:443;

        # 必要的代理头,保留原始 Host(按需)
        proxy_set_header Host $host;                    # 或改成 $proxy_host / example.com
        proxy_set_header X-Real-IP $remote_addr;        # 把客户端 IP 放入 X-Real-IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

采用的证书建议是使用Cloudflare生成的源服务器证书,因为可以设置15年有效期,有效防止证书忘记更新的情况。
这里别忘了,要把代理服务器的hosts文件改一下,因为如果在内网直接配置IP回源的话,会被自己匹配到默认站点而不是网站,你也看到了我配置文件回源的配置是proxy_pass https://www.nekopara.uk:443;,所以你需要修改/etc/hosts文件,加上以下内容:

192.168.13.229 nekopara.uk www.nekopara.uk

其中,192.168.13.229是主服务器的IP地址,后面则是匹配的域名,这样子反代后的流量走到主服务器就不会匹配默认站点了。

主服务器配置

修改你主服务器网站的Nginx配置文件,在服务器块的前面部分加上下面的内容:

set_real_ip_from 192.168.1.255;
real_ip_header X-Forwarded-For;

这是信任内网的服务器提供的远端IP地址,避免在网站的访问日志里面看到访问的IP全是代理服务器的内网IP。
完整的配置示例文件如下:

# Typecho HTTPS服务器块
server {
    listen 443 ssl;
    server_name www.nekopara.uk;
    
    #信任并设置代理服务器提供的远端IP
    set_real_ip_from 192.168.1.255;
    real_ip_header X-Forwarded-For;

    # 设置最大上传文件大小为 32M
    client_max_body_size 32M;

    # SSL证书和密钥路径
    ssl_certificate /data/certs/nekopara_uk.pem;
    ssl_certificate_key /data/certs/nekopara_uk.key;

    # SSL设置(可选但推荐用于安全)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # 网站根目录
    root /data/typecho;
    index index.html index.php;

    # Serve static files directly
    location / {
        try_files $uri $uri/ /index.php?$args;
        autoindex off;
    }


    # PHP处理,并启用FastCGI缓存
    location ~ \.php$ {
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm82/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;

        # 启用 FastCGI 缓存
        fastcgi_cache my_fastcgi_cache;
        fastcgi_cache_valid 200 301 302 404 10s; # 设置缓存的有效时间
        fastcgi_cache_methods GET HEAD; # 只缓存 GET 和 HEAD 请求

        # 不缓存 POST 请求和其他非幂等性请求
        set $skip_cache 0;
        if ($request_method = POST) {
            set $skip_cache 1;
        }
        if ($query_string != "") {
            set $skip_cache 1;
        }
        # 如果存在特定会话 cookie,则不缓存
        if ($http_cookie ~* "PHPSESSID|__typecho_uid|__typecho_authCode") {
            set $skip_cache 1;
        }

        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        # 添加头部信息显示缓存状态
        add_header X-FastCGI-Cache $upstream_cache_status;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }
}

验证代理情况下真实IP传递效果

我采用面向LLM编程写了下面的测试代码,用于展示客户端访问IP的情况,保存为ip.php放到网站目录访问即可看到效果:

<?php
// 获取客户端IP地址
$ip = $_SERVER['REMOTE_ADDR'];
$ip2 = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '未通过代理/CDN';

// 显示IP地址(带简单HTML格式)
echo "<h1>您的IP地址信息</h1>";

// 显示REMOTE_ADDR(直接连接IP)
echo "<div style='margin: 15px 0; padding: 15px; background: #f5f5f5; border-radius: 8px;'>";
echo "<strong>REMOTE_ADDR:</strong> <span style='color: #d32f2f; font-size: 18px;'>" . htmlspecialchars($ip) . "</span><br>";
echo "<small style='color: #666;'>通常是直接连接服务器的IP(可能是代理或真实IP)</small>";
echo "</div>";

// 显示HTTP_X_FORWARDED_FOR(原始客户端IP)
echo "<div style='margin: 15px 0; padding: 15px; background: #e8f5e8; border-radius: 8px;'>";
echo "<strong>HTTP_X_FORWARDED_FOR:</strong> <span style='color: #388e3c; font-size: 18px;'>" . htmlspecialchars($ip2) . "</span><br>";
echo "<small style='color: #666;'>通常表示原始客户端IP(仅当使用代理/CDN时存在)</small>";
echo "</div>";

// 添加说明
echo "<div style='text-align: center; margin-top: 30px; color: #666; font-size: 14px; line-height: 1.5; padding: 15px; background: #fff3e0; border-radius: 8px;'>";
echo "<strong>说明:</strong><br>";
echo "• 如果您未使用代理或CDN,HTTP_X_FORWARDED_FOR 可能为空或显示'未通过代理/CDN'<br>";
echo "• 如果您使用了CDN(如Cloudflare)、代理或负载均衡,HTTP_X_FORWARDED_FOR 通常包含您真实的IP地址<br>";
echo "• REMOTE_ADDR 通常是与服务器直接建立连接的设备IP";
echo "</div>";
?>

通过主服务器IP直接访问会在REMOTE_ADDR显示客户端真实的IP地址,HTTP_X_FORWARDED_FOR显示未通过代理/CDN;通过代理服务器访问则REMOTE_ADDR显示客户端真实的IP地址,HTTP_X_FORWARDED_FOR也显示客户端真实的IP地址。这才是配置正确的情况。
这里有一个小技巧,如果需要测试代理的可访问性,不需要配置DNS,这样反而会暴露备用服务器的IP地址,可以编辑自己电脑的hosts文件,指定主机名对应的IP为代理服务器IP进行测试即可。
而配置错误的情况下,REMOTE_ADDRHTTP_X_FORWARDED_FOR会显示VPC内另一个服务器的内网IP地址,这就需要重新配置,毕竟会影响网站的访问记录。
直接访问的情况:
02.png
通过代理访问的情况:
03.png
配置错误的情况:
04.png
05.png
注意⚠️:测试完成后请删除ip.php文件,避免服务器信息泄漏。

配置自动切换脚本

我们需要在备用服务器上面运行一个监测脚本监测主服务器状态,当主服务器被黑洞时可以更改Cloudflare DNS配置让流量通过Cloudflare代理后回源,避免访问中断且保护备用服务器。
这部分就和之前那篇文章是一样的了,基本上没有什么变化:

#!/bin/bash

# 🐱 Cloudflare API Token
CF_API_TOKEN="neko????????????????????????????????para"
# 🐱 域名,例如 nekopara.uk
CF_ZONE_NAME="nekopara.uk"
# 🐱 记录名称列表,例如 "nekopara.uk www.nekopara.uk"
CF_RECORD_NAMES=(nekopara.uk www.nekopara.uk)
# 🐱 目标 IP
TARGET_IP="119.8.185.128"
# 🐱 目标端口
TARGET_PORT="80"
# 🐱 备用 IP
BACKUP_IP="172.16.1.233"

# 🐱 获取 Zone ID
get_zone_id() {
  curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${CF_ZONE_NAME}" \
       -H "Authorization: Bearer ${CF_API_TOKEN}" \
       -H "Content-Type: application/json" | jq -r '.result[0].id'
}

# 🐱 获取 DNS 记录 ID
get_record_id() {
  local zone_id="$1"
  local record_name="$2"
  curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=A&name=${record_name}" \
       -H "Authorization: Bearer ${CF_API_TOKEN}" \
       -H "Content-Type: application/json" | jq -r '.result[0].id'
}

# 🐱 更新 DNS 记录
update_dns_record() {
  local zone_id="$1"
  local record_id="$2"
  local record_name="$3"
  local ip="$4"
  local proxied="$5"
  curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" \
       -H "Authorization: Bearer ${CF_API_TOKEN}" \
       -H "Content-Type: application/json" \
       --data "{\"type\":\"A\",\"name\":\"${record_name}\",\"content\":\"${ip}\",\"ttl\":1,\"proxied\":${proxied}}" | jq
}

# 🐱 检测目标 IP 的 HTTP 状态码
check_target() {
  curl -s --connect-timeout 10 "http://${TARGET_IP}:${TARGET_PORT}" -o /dev/null
  return $?
}

# 🐱 初始化
zone_id=$(get_zone_id)
declare -A record_ids
for record_name in "${CF_RECORD_NAMES[@]}"; do
  record_ids["$record_name"]=$(get_record_id "$zone_id" "$record_name")
done

current_ip="$TARGET_IP"
proxied=false
failure_count=0
success_count=0

# 🐱 监控循环
while true; do
  if check_target; then
    echo "🐾 目标 IP ${TARGET_IP} 响应正常"
    failure_count=0
    success_count=$((success_count + 1))
    if [ "$current_ip" != "$TARGET_IP" ] && [ "$success_count" -ge 3 ]; then
      echo "🐾 切换回原始 IP ${TARGET_IP},关闭代理"
      for record_name in "${CF_RECORD_NAMES[@]}"; do
        update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$TARGET_IP" false
      done
      current_ip="$TARGET_IP"
      proxied=false
      success_count=0
    fi
  else
    echo "🐾 目标 IP ${TARGET_IP} 无响应"
    failure_count=$((failure_count + 1))
    success_count=0
    if [ "$current_ip" != "$BACKUP_IP" ] && [ "$failure_count" -ge 3 ]; then
      echo "🐾 切换到备用 IP ${BACKUP_IP},开启代理"
      for record_name in "${CF_RECORD_NAMES[@]}"; do
        update_dns_record "$zone_id" "${record_ids[$record_name]}" "$record_name" "$BACKUP_IP" true
      done
      current_ip="$BACKUP_IP"
      proxied=true
      failure_count=0
    fi
  fi
  sleep 60
done

详细的配置方法,请参考《当网站遭遇DDoS:如何优雅地应对云服务器被封堵》,这里不再赘述。

小尾巴

最后还有一点别忘了,那就是将VPC内网IP加入iptables访问白名单,如果你按照我之前的文章设置过单IP访问连接数限制的话。
在规则列表前面添加白名单:

iptables -I INPUT 1 -p tcp -s 192.168.0.0/16 --dport 80 -j ACCEPT
iptables -I INPUT 2 -p tcp -s 192.168.0.0/16 --dport 443 -j ACCEPT

如果不这么做,当流量变大时,VPC内网的代理服务器的请求就会全部被拦截,导致访问失败,跟网站挂掉没什么两样了。(我 防 我 自 己)

总结

这样以来,我们就成功构建了一个在平时能够快速访问,而在被DDoS时又不会彻底挂掉的网站,用较低的成本平衡了访问速度和抗DDoS能力。在平时直接走服务器的IP提升访问速度,而在被DDoS时,通过Cloudflare代理保护源服务器。
其工作的逻辑和过程大概如下:

  • 当被DDoS攻击时:主服务器EIP被打黑洞无法访问 > 备用服务器监测到主服务器无法访问 > 切换为Cloudflare代理流量,并更改DNS解析记录到备用服务器的EIP > 网站继续可访问,但是速度变慢
  • 当DDoS停止,主服务器EIP解封:备用服务器监测到主服务器正常访问 > 关闭Cloudflare代理流量,并更改DNS解析记录到主服务器的EIP > 网站恢复访问速度