谷歌云!将白嫖进行到底
Google Cloud 免费计划 {#Google-Cloud-免费计划.wp-block-heading}
具体步骤可参考这篇文章 谷歌云Free Tier长期免费云服务器
-
1 non-preemptible e2-micro VM instance per month in one of the following US regions:
- Oregon: us-west1
- Iowa: us-central1
- South Carolina: us-east1
- 30 GB-months standard persistent disk (标准永久性磁盘HDD)
- 1 GB of outbound data transfer from North America to all region destinations (excluding China and Australia) per month
- 网络选择 标准网络层级 每月有200g的免费流量,
另外创建虚拟机实例页面有两个坑
数据保护中的备份要去掉,选择无备份
可观测性中 Ops Agent 安装要去掉,这是用于监控和日志录的,按量付费
创建交换分区 {#创建交换分区.wp-block-heading}
由于 e2-micro 只有1G 内存,创建交换分区可以缓解内存不足的状况
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
使交换空间永久生效,编辑 /etc/fstab 文件,添加以下内容:
/swapfile none swap sw 0 0
优化交换使用策略,编辑 /etc/sysctl.conf,添加:
vm.swappiness=10
Cloudflare ddns {#Cloudflare-ddns.wp-block-heading}
按照免费计划创建的 e2-micro 还有一个缺陷就是公网 IP 是临时性的,关机后会更换 IP 地址。这个可以用 ddns 解决,将域名托管在 cloudflare ,通过cloudflare API 动态更新域名的解析地址,以后地址再怎么变也不怕
后面就可以安心用域名 ssh 登录,下面是脚本每次重启自动运行一次,推荐使用 systemd 服务而不用 crontab,systemd 服务可确保在网络准备好之后再运行脚本
#!/bin/bash
# --- Configuration ---
AUTH_EMAIL="" # Optional: Older API key method
API_TOKEN="" # !!! 必须修改: Your Cloudflare API Token !!!
ZONE_ID="" # !!! 必须修改: Your Zone ID !!!
RECORD_TYPE="A" # !!! 必须修改: Record type ("A" or "AAAA") !!!
LOG_FILE="" # !!! 检查确认: Log file path and permissions !!!
IP_CACHE_FILE="" # !!! 检查确认: Cache file path and permissions !!!
SLEEP_BETWEEN_RECORDS=1 # Seconds to wait between processing records (0 to disable)
# !!! 必须修改: List of DNS records to update !!!
RECORD_NAMES=(
"baidu.com"
"sb.baidu.com"
# Add more records here...
)
# Public IP lookup services (primary and backups)
if [ "<span class="katex math inline">RECORD_TYPE" == "A" ]; then
IP_SERVICES=("https://api.ipify.org" "https://icanhazip.com" "https://ipinfo.io/ip")
IP_TYPE_DESC="IPv4"
elif [ "</span>RECORD_TYPE" == "AAAA" ]; then
IP_SERVICES=("https://api64.ipify.org" "https://icanhazip.com" "https://ipinfo.io/ip")
IP_TYPE_DESC="IPv6"
else
# Log initial config error to file if possible, otherwise echo
err_msg="<span class="katex math inline">(date '+%Y-%m-%d %H:%M:%S') - Error: Unsupported RECORD_TYPE:</span>RECORD_TYPE"
if [ -n "<span class="katex math inline">LOG_FILE" ] && [ -d "</span>(dirname "<span class="katex math inline">LOG_FILE")" ] && [ -w "</span>(dirname "<span class="katex math inline">LOG_FILE")" ]; then
echo "</span>err_msg" >> "<span class="katex math inline">LOG_FILE"
else
echo "</span>err_msg"
fi
exit 1
fi
# --- End Configuration ---
# --- Helper function for logging ---
log() {
local message="<span class="katex math inline">1"
local log_entry="</span>(date '+%Y-%m-%d %H:%M:%S') - <span class="katex math inline">message"
# Try to log to file, fallback to echo if file/dir not writable or not set
if [ -n "</span>LOG_FILE" ]; then
if ! echo "<span class="katex math inline">log_entry" >> "</span>LOG_FILE" 2>/dev/null; then
echo "<span class="katex math inline">log_entry (Error writing to log file:</span>LOG_FILE)"
fi
else
echo "<span class="katex math inline">log_entry"
fi
}
# --- Function to get current public IP with redundancy ---
get_current_ip() {
local ip=""
for service in "</span>{IP_SERVICES[@]}"; do
log "Attempting to get public IP from <span class="katex math inline">service..."
ip=</span>(curl -s --connect-timeout 5 "<span class="katex math inline">service")
if [[ -n "</span>ip" && "<span class="katex math inline">ip" =~ ^[0-9a-fA-F.:]+</span> ]]; then
log "Successfully retrieved IP: <span class="katex math inline">ip from</span>service"
echo "<span class="katex math inline">ip"
return 0
else
log "Failed or received invalid response from</span>service (Response: <span class="katex math inline">ip)"
fi
done
log "Error: Could not retrieve current public IP from any service."
return 1
}
# --- Main Logic ---
# 1. Get current public IP address
CURRENT_IP=</span>(get_current_ip)
if [ <span class="katex math inline">? -ne 0 ]; then
exit 1
fi
log "Current public</span>IP_TYPE_DESC IP is: <span class="katex math inline">CURRENT_IP"
# 2. Compare with cached IP
CACHED_IP=""
if [ -f "</span>IP_CACHE_FILE" ]; then
CACHED_IP=<span class="katex math inline">(cat "</span>IP_CACHE_FILE")
log "Last known IP from cache (<span class="katex math inline">IP_CACHE_FILE):</span>CACHED_IP"
fi
if [ "<span class="katex math inline">CURRENT_IP" == "</span>CACHED_IP" ]; then
log "Public IP (<span class="katex math inline">CURRENT_IP) matches cached IP. No Cloudflare update needed."
exit 0
fi
log "Public IP (</span>CURRENT_IP) differs from cached IP (<span class="katex math inline">CACHED_IP) or cache is empty. Proceeding with Cloudflare checks..."
# --- Loop through each record name ---
log "Starting DNS update process for</span>{#RECORD_NAMES[@]} record(s)..."
ALL_SUCCESS=true
UPDATES_PERFORMED=false
for RECORD_NAME in "<span class="katex math inline">{RECORD_NAMES[@]}"; do
log "--- Processing record:</span>RECORD_NAME ---"
# 3. Get the DNS Record ID, current IP, and **Proxied Status** for THIS record from Cloudflare
RECORD_INFO=<span class="katex math inline">(curl -s --connect-timeout 10 -X GET "https://api.cloudflare.com/client/v4/zones/</span>ZONE_ID/dns_records?type=<span class="katex math inline">RECORD_TYPE&name=</span>RECORD_NAME" \
-H "Authorization: Bearer <span class="katex math inline">API_TOKEN" \
-H "Content-Type: application/json")
RECORD_SUCCESS=</span>(echo "<span class="katex math inline">RECORD_INFO" | jq -r '.success')
if [ "</span>RECORD_SUCCESS" != "true" ]; then
log "Error querying DNS record for '<span class="katex math inline">RECORD_NAME'. API Response:</span>RECORD_INFO"
ALL_SUCCESS=false
continue
fi
RECORD_COUNT=<span class="katex math inline">(echo "</span>RECORD_INFO" | jq '.result | length')
if [ "<span class="katex math inline">RECORD_COUNT" -eq 0 ]; then
log "Error: DNS record '</span>RECORD_NAME' (<span class="katex math inline">RECORD_TYPE) not found. Skipping."
ALL_SUCCESS=false
continue
fi
if [ "</span>RECORD_COUNT" -gt 1 ]; then
log "Warning: Multiple records found for '<span class="katex math inline">RECORD_NAME' (</span>RECORD_TYPE). Using the first one."
fi
# Extract ID, IP, and Proxied Status
RECORD_ID=<span class="katex math inline">(echo "</span>RECORD_INFO" | jq -r '.result[0].id')
RECORD_IP=<span class="katex math inline">(echo "</span>RECORD_INFO" | jq -r '.result[0].content')
RECORD_PROXIED_STATUS=<span class="katex math inline">(echo "</span>RECORD_INFO" | jq -r '.result[0].proxied') # <<< 添加: 获取代理状态
# Validate all extracted values
if [ -z "<span class="katex math inline">RECORD_ID" ] || [ "</span>RECORD_ID" == "null" ] || \
[ -z "<span class="katex math inline">RECORD_IP" ] || [ "</span>RECORD_IP" == "null" ] || \
[ -z "<span class="katex math inline">RECORD_PROXIED_STATUS" ] || [ "</span>RECORD_PROXIED_STATUS" == "null" ]; then # <<< 添加: 验证代理状态
log "Error: Could not retrieve valid DNS Record ID, IP, or Proxied status for '<span class="katex math inline">RECORD_NAME'. Response:</span>RECORD_INFO. Skipping."
ALL_SUCCESS=false
continue
fi
log "Cloudflare DNS IP for <span class="katex math inline">RECORD_NAME is:</span>RECORD_IP, Proxied: <span class="katex math inline">RECORD_PROXIED_STATUS" # <<< 修改: 日志包含代理状态
# 4. Compare IPs and update THIS record if necessary, preserving proxied status
if [ "</span>CURRENT_IP" != "<span class="katex math inline">RECORD_IP" ]; then
log "IP has changed for</span>RECORD_NAME (<span class="katex math inline">RECORD_IP -></span>CURRENT_IP). Updating Cloudflare DNS record..."
UPDATE_RESPONSE=<span class="katex math inline">(curl -s --connect-timeout 10 -X PUT "https://api.cloudflare.com/client/v4/zones/</span>ZONE_ID/dns_records/<span class="katex math inline">RECORD_ID" \
-H "Authorization: Bearer</span>API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"<span class="katex math inline">RECORD_TYPE\",\"name\":\"</span>RECORD_NAME\",\"content\":\"<span class="katex math inline">CURRENT_IP\",\"ttl\":1,\"proxied\":</span>RECORD_PROXIED_STATUS}") # <<< 修改: 使用变量 <span class="katex math inline">RECORD_PROXIED_STATUS
UPDATE_SUCCESS=</span>(echo "<span class="katex math inline">UPDATE_RESPONSE" | jq -r '.success')
if [ "</span>UPDATE_SUCCESS" == "true" ]; then
log "Successfully updated DNS record for <span class="katex math inline">RECORD_NAME to</span>CURRENT_IP (Proxied status kept as <span class="katex math inline">RECORD_PROXIED_STATUS)."
UPDATES_PERFORMED=true
else
log "Error updating DNS record for</span>RECORD_NAME. API Response: <span class="katex math inline">UPDATE_RESPONSE"
ALL_SUCCESS=false
fi
else
log "Cloudflare IP for</span>RECORD_NAME already matches current public IP (<span class="katex math inline">CURRENT_IP). No update needed."
fi
# Optional sleep between records
if [ "</span>SLEEP_BETWEEN_RECORDS" -gt 0 ]; then
log "Waiting <span class="katex math inline">{SLEEP_BETWEEN_RECORDS}s before next record..."
sleep "</span>SLEEP_BETWEEN_RECORDS"
fi
done # --- End of loop for RECORD_NAMES ---
# 5. Update cache file if the process completed without critical errors preventing updates and IP changed
# (We update cache even if some non-critical errors occurred for individual records,
# as long as we successfully determined the new public IP differs from the old cache)
if [ "<span class="katex math inline">CURRENT_IP" != "</span>CACHED_IP" ]; then
# Check if we should update the cache. We update if *any* update was attempted or if cache was initially empty.
# We might choose *not* to update if ALL attempts failed, but let's update if the public IP genuinely changed.
log "Updating IP cache file <span class="katex math inline">IP_CACHE_FILE with new IP:</span>CURRENT_IP"
echo "<span class="katex math inline">CURRENT_IP" > "</span>IP_CACHE_FILE"
# More conservative approach: only update cache if <span class="katex math inline">ALL_SUCCESS is true AND</span>UPDATES_PERFORMED is true.
# if <span class="katex math inline">ALL_SUCCESS &&</span>UPDATES_PERFORMED; then ... echo ... fi
fi
log "Finished processing all records."
if $ALL_SUCCESS; then
exit 0
else
log "One or more errors occurred during the update process. Check logs above."
exit 1
fi
填入开头的变量,可遍历修改同个ID域的多个二级域名,其中会生成 log 文件,和 ip 缓存文件, ip 缓存文件用于比对 ip 免于频繁访问 API 触发风控。
在 /etc/systemd/system/ 目录下创建 systemd 服务单元文件 cloudflare-ddns.service
[Unit]
Description=Cloudflare DDNS Update Script
Wants=network-online.target # 需要网络连接后才运行
After=network-online.target # 在网络连接之后运行
[Service]
Type=oneshot # 只需要运行一次
ExecStart= # 脚本的完整路径
RemainAfterExit=yes # 即使 ExecStart 完成,服务也视为活动 (可选,对 oneshot 有用)
# 如果你的脚本需要以特定用户运行,可以取消下面一行的注释
# User=guest
# 注意:如果以特定用户运行,确保该用户对日志文件/目录有写入权限
[Install]
WantedBy=multi-user.target # 在多用户模式下启用
重新加载 systemd 配置,设置服务开机自启
sudo systemctl daemon-reload
sudo systemctl enable cloudflare-ddns.service
立即运行一次服务来测试它,检查服务状态(cloudflare-ddns.service需要将注释去掉,不然会报错)
sudo systemctl start cloudflare-ddns.service
sudo systemctl status cloudflare-ddns.service
安全防护 CrowdSec {#安全防护-CrowdSec.wp-block-heading}
服务器当然要对外服务,难免要开放个别端口,如果只是用来 ssh remote 完全可以安装 VPN 只对自己开放( IP 地址会经常被封),为了避免被反薅需要特别的安全防护,长亭雷池太吃资源所以选择了 CrowdSec ,有总比没有好
直接按照官网走就好,注册了账号会提示,其中安全组件,我只安装了防火墙和 Nginx,规则订阅的话因为没有会员选择了官方推荐的 Firehol greensnow.co list 和 Free Proxies list
网络达量停机 {#网络达量停机.wp-block-heading}
云主机最怕的就是被刷流量,一觉醒来一套房就没了,采用基于 vnstat 的流量监控和自动停机方案可以防止流量超标,
为云主机实现网络达量停机
上面是我参考的博客文章,不过里面的脚本也太简洁了,我还是让 ChatGPT 帮我另外写了一个脚本,每分钟执行一次 sudo crontab e,因为关机需要 sudo 操作
#!/bin/bash
#
# 脚本名称: check_traffic_limit.sh
# 描述: 监控指定网络接口的月度流量,并在达到限制时关闭服务器。
# 作者: (根据用户需求编写)
# 日期: 2025-04-12
#
# 注意: 此脚本需要以 root 权限运行 (例如通过 sudo) 才能执行关机操作
# 并且需要确保 vnstat, jq 命令已安装。
# 建议通过 cron 定期执行此脚本。
#
# --- 配置项 ---
INTERFACE="ens4" # 要监控的网络接口名称
LIMIT_GB=200 # 流量限制 (单位: GB)
BYTES_PER_GB=1000000000 # 每 GB 的字节数 (10^9, SI 标准)
LOG_FILE="/var/log/traffic_limit_check.log" # 日志文件路径
# --- 计算字节单位的限制值 ---
LIMIT_BYTES=<span class="katex math inline">((LIMIT_GB * BYTES_PER_GB))
# --- 命令路径 (自动查找) ---
# 使用 command -v 查找命令路径,更可靠
VNSTAT_CMD=</span>(command -v vnstat)
JQ_CMD=<span class="katex math inline">(command -v jq)
SHUTDOWN_CMD="/sbin/shutdown" #crontab 可能没有 PATH 环境变量 使用 sudo which shutdown 来查找
AWK_CMD=</span>(command -v awk)
DATE_CMD=<span class="katex math inline">(command -v date)
# --- 脚本选项 ---
# set -o pipefail # 让管道命令的返回值取最后一个非零返回值 (如果需要更严格的错误处理)
# --- 日志记录函数 ---
log_message() {
# 将带有时间戳的消息追加到日志文件
echo "</span>(<span class="katex math inline">DATE_CMD '+%Y-%m-%d %H:%M:%S') -</span>1" >> "<span class="katex math inline">LOG_FILE"
}
# --- 依赖检查 ---
# 检查必要的命令是否存在
if [ -z "</span>VNSTAT_CMD" ]; then
log_message "错误: 未找到 'vnstat' 命令。请安装 vnstat 并确保其在 PATH 中。"
exit 1
fi
if [ -z "<span class="katex math inline">JQ_CMD" ]; then
log_message "错误: 未找到 'jq' 命令。请安装 jq 并确保其在 PATH 中。"
exit 1
fi
if [ -z "</span>SHUTDOWN_CMD" ]; then
# 注意: shutdown 通常需要 root 权限才能找到或执行
log_message "错误: 未找到 'shutdown' 命令。请确保以 root 权限运行,或命令路径正确。"
exit 1
fi
if [ -z "<span class="katex math inline">AWK_CMD" ]; then
log_message "错误: 未找到 'awk' 命令。"
exit 1
fi
if [ -z "</span>DATE_CMD" ]; then
# 这个几乎不可能找不到,但以防万一
log_message "错误: 未找到 'date' 命令。"
exit 1
fi
# --- 主要逻辑 ---
log_message "====== 开始检查接口 <span class="katex math inline">INTERFACE 的流量 ======"
log_message "配置限制:</span>LIMIT_GB GB (<span class="katex math inline">LIMIT_BYTES 字节)"
# 获取指定接口的月度流量数据 (JSON 格式)
# 使用 -i 指定接口,--json m 获取月度 json
JSON_DATA=</span>(<span class="katex math inline">VNSTAT_CMD --json m -i "</span>INTERFACE")
VNSTAT_EXIT_CODE=<span class="katex math inline">?
# 检查 vnstat 命令是否成功执行
if [</span>VNSTAT_EXIT_CODE -ne 0 ]; then
log_message "错误: vnstat 命令执行失败 (接口: <span class="katex math inline">INTERFACE, 退出码:</span>VNSTAT_EXIT_CODE)。请检查 vnstat 是否正确配置并运行。"
# 可以选择记录 vnstat 可能的错误输出 (如果有的话)
# log_message "vnstat 输出: <span class="katex math inline">JSON_DATA" # 注意 JSON_DATA 此时可能包含错误信息而非 JSON
exit 1
fi
# 使用 jq 解析 JSON 获取当月的 rx 和 tx 字节数
# **重要修正**: 使用 .name 选择接口,而不是 .id
# 使用 '.interfaces[] | select(.name ==</span>iface) | .traffic.month[0]' 定位到当月数据
# 然后分别提取 .rx 和 .tx,使用 // 0 提供默认值以防字段缺失或为 null
CURRENT_MONTH_RX_BYTES=<span class="katex math inline">(echo "</span>JSON_DATA" | <span class="katex math inline">JQ_CMD --arg iface "</span>INTERFACE" '.interfaces[] | select(.name == <span class="katex math inline">iface) | .traffic.month[0].rx // 0')
CURRENT_MONTH_TX_BYTES=</span>(echo "<span class="katex math inline">JSON_DATA" |</span>JQ_CMD --arg iface "<span class="katex math inline">INTERFACE" '.interfaces[] | select(.name ==</span>iface) | .traffic.month[0].tx // 0')
# 验证 jq 是否成功提取了有效的数值 (必须是非负整数)
# 使用正则表达式检查变量是否只包含数字
if ! [[ "<span class="katex math inline">CURRENT_MONTH_RX_BYTES" =~ ^[0-9]+</span> ]]; then
log_message "错误: 未能从 <span class="katex math inline">INTERFACE 的 vnstat JSON 中解析出有效的 RX 字节数。获取到的值: '</span>CURRENT_MONTH_RX_BYTES'"
log_message "请手动执行 'sudo vnstat --json m -i <span class="katex math inline">INTERFACE' 检查 JSON 输出内容。"
# 如果需要调试,取消下面这行的注释以记录完整的 JSON 数据
# echo "</span>JSON_DATA" >> "<span class="katex math inline">LOG_FILE"
exit 1
fi
if ! [[ "</span>CURRENT_MONTH_TX_BYTES" =~ ^[0-9]+<span class="katex math inline">]]; then
log_message "错误: 未能从</span>INTERFACE 的 vnstat JSON 中解析出有效的 TX 字节数。获取到的值: '<span class="katex math inline">CURRENT_MONTH_TX_BYTES'"
log_message "请手动执行 'sudo vnstat --json m -i</span>INTERFACE' 检查 JSON 输出内容。"
# 如果需要调试,取消下面这行的注释以记录完整的 JSON 数据
# echo "<span class="katex math inline">JSON_DATA" >> "</span>LOG_FILE"
exit 1
fi
# 计算当月总字节数 (RX + TX)
CURRENT_TOTAL_BYTES=<span class="katex math inline">((CURRENT_MONTH_RX_BYTES + CURRENT_MONTH_TX_BYTES))
# 将当前总字节数转换为 GB (保留两位小数) 以便日志阅读
# 使用 awk 进行浮点数计算
CURRENT_TOTAL_GB=</span>(<span class="katex math inline">AWK_CMD "BEGIN {printf \"%.2f\",</span>CURRENT_TOTAL_BYTES / <span class="katex math inline">BYTES_PER_GB}")
log_message "</span>INTERFACE 当前月度总用量: <span class="katex math inline">CURRENT_TOTAL_GB GB (</span>CURRENT_TOTAL_BYTES 字节)。"
# 比较当前用量与限制值 (大于或等于)
if [ "<span class="katex math inline">CURRENT_TOTAL_BYTES" -ge "</span>LIMIT_BYTES" ]; then
log_message "警告: 接口 <span class="katex math inline">INTERFACE 的月度流量 (</span>CURRENT_TOTAL_GB GB) 已达到或超出限制 (<span class="katex math inline">LIMIT_GB GB)。"
log_message "====== 正在执行关机操作! ======"
# 执行立即关机 - 需要 ROOT 权限
"</span>SHUTDOWN_CMD" -h now "系统关机:接口 <span class="katex math inline">INTERFACE 的月度网络流量 (</span>CURRENT_TOTAL_GB GB) 已达到 $LIMIT_GB GB 的限制。"
# 关机命令发出后,脚本可能不会执行到 exit 0
exit 0 # 理论上关机后不会执行到这里
else
log_message "状态: 流量用量在限制范围内。"
log_message "====== 检查完成 ======"
exit 0 # 正常退出
fi
脚本每分钟执行一次产生的 log 文件,可以用日志轮转(logrotate)解决
谷歌官方超预算停机 {#谷歌官方超预算停机.wp-block-heading}
查阅了谷歌云文档发现,官方可以实现超预算停机的效果,这不巧了吗
这是官方的流程图很好理解,创建一个预算和提醒,当费用超过预算时,会发送邮件和 Pub/Sub 通知,而Pub/Sub 通知可以触发 Cloud run Function 来停用结算账号避免产生超额费用。
- 首先创建一个预算,勾选邮件提醒和关联 Pub/Sub 通知,关联 Pub/Sub 通知会要求你新建一个主题
- IAM 中新建服务账号,授予 roles/billing.admin 权限,作为 运行时服务账号
-
进入 Pub/Sub 界面找到刚创建的主题,在主题的
更多操作中触发 Cloud Run 函数,其中代码直接粘贴文档代码,不设置环境变量话,其中PROJECT_ID修改为你实际的项目 ID - Pub/Sub 主题中发布消息测试
走了一遍流程发现其中的难点在于权限
-
Pub/Sub 服务 (
service-<PROJECT_NUMBER>@gcp-sa-pubsub.iam.gserviceaccount.com) 需要Cloud Run Invoker权限来触发/调用你的函数 - 函数需要调用 Billing API 的权限: 当你的函数代码运行时,去调用 Cloud Billing API 来执行停用结算的操作。
要解决上面两个问题,在创建 Cloud run Function 时,直接用默认的Compute Engine default service account 为 触发器服务账号(默认),运行时服务账号 需要在 IAM 中新建,并授予其 Billing Account Administrator 角色
还有用文档中的 node.js ,用 Python 好多次没有成功。
信用卡限额 {#信用卡限额.wp-block-heading}
最好绑定一张虚拟信用卡,真实的信用卡的话一定要限制额度