宽带没有公网 IPv4 但是有 IPv6,路由器经过前面《路由器防火墙开放指定后缀的动态 IPv6 并永久保存》的设置后,我们可以随时随地地访问家里有 IPv6 地址的内部设备,而且只开放需要的指定设备,安全可控、速度也不错。

联通宽带分配的 IPv6 地址虽然是公网的,但是是动态的、变化的,如果我们要访问内网设备,需要知道当前的地址是什么?

直接访问

地址变化实时通知提醒

最简单的是每次地址变化后,通知用户,用户使用最新变化后地址访问。

此方法耗人力,每次地址不一样,使用需要更改到最新地址,优点是不用其它额外费用,比如宽带需要公网IPv4、额外购买公网VPS、购买域名等等额外费用。

# 伪代码
# 1. 获取当前 IPv6
current_ipv6=$(ip -6 addr show  dev eth0 | grep global | awk '{print $2}' | awk -F "/" '{print $1}' | grep 2408 | head -n 1)
# 2. 手机通知提醒
curl "http://192.168.0.117/notify?title=IPv6 changed&description=Gateway117 http://[${current_ipv6}]:51080"

绑定域名访问

先了解两个概念,来自维基百科:

  • DNS,域名系统(Domain Name System)是一项互联网服务。主要作用是将域名和 IP 地址相互映射,使人更方便地访问互联网。
  • DDNS,动态DNS(Dynamic DNS)是域名系统(DNS)中的一种自动更新名称服务器(Name server)内容的技术。它提供一个机制,让本地 DNS 数据库即时更新。它能把互联网域名,指往一个可能经常改变的 IP 地址。让互联网上的外界用户可以透过一个固定的域名,连接到一个可能经常动态改变 IP 地址的机器。

通过 DDNS,每次 IPv6 地址变化,更新域名指向的地址,这样就可以使用固定的域名访问。

此方法使用方便,地址为固定域名,缺点是需要额外购买一个域名,而且域名托管系统支持 DDNS 功能,通过 API 即使更改 DNS 记录。

完整脚本(脱敏和排版删除部分注释版):

#!/usr/bin/env bash

# ----------------------------------------------
# ipv6 ddns Cloudflare 动态域名解析
# author: blog.196000.xyz
# */3 * * * * cd /opt/scripts && ./ddns-ipv6-cf.sh >> /dev/null 2>&1
# ----------------------------------------------

# 脚本错误终止
set -o errexit
# 未定义变量时终止运行
set -o nounset
# 管道子命令失败脚本终止
set -o pipefail

# 输出执行命令
#set -x 
 
# 配置 Cloudflare 参数
#=========================================================================
# Account email
[email protected]

# Hostname to update, eg: ip6.example.com
DOMAIN=ipv6.196000.xyz

# Global API Key    
# https://dash.cloudflare.com/profile/api-tokens
API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Zone ID
# https://dash.cloudflare.com
# Dashboard - Websites - YourDomain(eg: example.com) - API - Zone ID
ZONE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#=========================================================================

# 配置 gateway 服务器参数
#=========================================================================
# 健康检查
HEALTH_PORT="38088"
LOG_FILE=/opt/scripts/.ddns.v6
#=========================================================================

# 函数定义
#=========================================================================
urlencode() {
  s="${1//'%'/%25}"
  s="${s//' '/%20}"
  s="${s//'"'/%22}"
  s="${s//'#'/%23}"
  s="${s//'$'/%24}"
  s="${s//'&'/%26}"
  s="${s//'+'/%2B}"
  s="${s//','/%2C}"
  s="${s//'/'/%2F}"
  s="${s//':'/%3A}"
  s="${s//';'/%3B}"
  s="${s//'='/%3D}"
  s="${s//'?'/%3F}"
  s="${s//'@'/%40}"
  s="${s//'['/%5B}"
  s="${s//']'/%5D}"
  printf %s "$s"
}

notify() {
  title=$1
  content=$2
  entitle=$(urlencode "$title")
  encontent=$(urlencode "$content")  
  reminder_response=$(curl -s "http://192.168.0.117/notify?title={$entitle}&description={$encontent}")
  echo $reminder_response
}
#=========================================================================

# 逻辑处理
#=========================================================================
if [ -e $LOG_FILE ]; then
  last_ipv6=`cat $LOG_FILE`
else
    # 默认 localhost
    last_ipv6='::1'
fi
# current_ipv6=$(ip -6 addr list scope global | grep -v " fd" | sed -n 's/.*inet6 \([0-9a-f:]\+\).*/\1/p' | head -n 1)
current_ipv6=$(ip -6 addr show dev eth0 | grep global | awk '{print $2}' | awk -F "/" '{print $1}' | grep 2408 | head -n 1)
#current_ipv6=2408:9174:678b:2fc5:7sd:61ff:fa9e:2a84
if [ "$last_ipv6" = "$current_ipv6" ]; then
  echo "IPv6($current_ipv6) not changed"
  exit
else
  echo "IPv6 changed"
  echo $last_ipv6
  echo $current_ipv6
  # Cloudflare API
  # https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-update-dns-record
  list_dns_record_api="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records"
  record_identifier_response=$(curl -s -X GET "$list_dns_record_api?name=$DOMAIN" -H "Content-Type: application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY")
  echo $record_identifier_response

  if [ "$record_identifier_response" != "${record_identifier_response%success*}" ] && [ "$(echo $record_identifier_response | grep "\"success\":true")" != "" ]; then
    # BusyBox v1.36.1 (2023-06-02 00:42:02 UTC) multi-call binary.
    # grep: unrecognized option: P
    # record_identifier=$(echo $record_identifier_response | grep -Po '(?<="id":")[^"]*' | head -1)
    record_identifier=$(echo $record_identifier_response | grep -o '"id":"[^"]*' | cut -f4- -d\")
    echo $record_identifier
    update_dns_record_api="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${record_identifier}";
    update_dns_record_response=$(curl -s -X PUT "$update_dns_record_api" -H "Content-Type:application/json" -H "X-Auth-Email: $EMAIL" -H "X-Auth-Key: $API_KEY" --data "{\"content\":\"$current_ipv6\", \"name\":\"$DOMAIN\",\"proxied\":false,\"type\":\"AAAA\",\"ttl\":300}")
    echo $update_dns_record_response    
    if [ "$update_dns_record_response" != "${update_dns_record_response%success*}" ] && [ "$(echo $update_dns_record_response | grep "\"success\":true")" != "" ]; then
      # save log
      echo $current_ipv6 > $LOG_FILE
      # notify
      title="IPv6 and DNS changed"
      content="Gateway117 http://[${current_ipv6}]:${HEALTH_PORT}"
      notify "${title}" "${content}"
      echo $title
      exit 0
    else
      # notify
      title="IPv6 changed but DNS fail"
      content="Gateway117 http://[${current_ipv6}]:${HEALTH_PORT}"
      notify "${title}" "${content}"
      echo $title
      exit 1
    fi
  else
    # notify
    title="IPv6 changed but DNS getid fail"
    content="Gateway117 http://[${current_ipv6}]:${HEALTH_PORT}"
    notify "${title}" "${content}"
    echo $title
    exit 1
  fi
fi