前言

在前面的文章中说到过,现在工信部大力推动三大运营商发展ipv6,对家宽而言,未来能使用的公网ipv4地址将会更加稀缺。在无公网ipv4的情况下,博主此前介绍过frp内网穿透和UDP打洞的方式来异网访问群晖NAS。然而frp速度会有限制,UDP打洞不一定成功(尤其是移动之类的网络),此前使用tailscale便遇到过失败的问题,所以折腾了几个小时,最终找到一套ipv6+ddns在公网中访问群晖的方式。

ipv6的优势

相信很多人都一定程度了解“可以让地球每一粒沙子都分配一个ip”的ipv6,本文不再过多赘述。由于ipv6的优势,使得我们生活中每一台设备都能有一个公网ipv6地址,加上工信部的助推,三大运营商几年前就已经开始将ipv6投入使用。重庆电信从2018年左右开始回收普通家宽的公网ipv4地址,为家庭宽带用户提供公网ipv6地址。

有了公网ipv6地址,在外访问家中的群晖速度比frp更快,成功率比UDP打洞更高,安全性更强。

使用条件

首要的条件便是宽带必须要能够获取并分配给设备ipv6地址。

但是大多数光猫默认是关闭了ipv6功能的,所以如果你是光猫拨号,那么需要超级管理员密码登陆光猫后台进行修改。如果光猫桥接,路由器拨号,在路由器中启用ipv6即可。不同的光猫和路由器的方法各不相同,所以在此无法提供一个准确的方法。

在设备获取到ipv6地址后要判断其是否为公网ipv6地址。

如果你的设备获取到的ipv6地址是fe80开头的,那么这个地址不能在公网中被访问。但是不要急,这并不意味着你的运营商就没有给公网地址,如果你能登陆光猫或者路由器,在后台如果能看到240e之类开头的地址,说明是可以获取到公网ipv6的。

当然还有一种更简单的方法,访问这个地址,如果有公网ipv6则会如下显示:

ipv6测试

我此前一直以为电信分配的是内网v6,后来才发现是因为我光猫拨号,路由器开启了DHCP,设备获取到的v6地址是由内网路由器分配的,解决办法也很简单:

  1. 光猫拨号,路由器设置桥接模式,所有设备ip均由光猫分配;
  2. 光猫桥接,路由器拨号,如果运营商能正常下发PD,路由器通常也能分配公网ipv6地址。

使用方法

测试连接

如果你能获取到公网ipv6地址,那么你可以在群晖DSM的控制面板中或者通过ssh命令查看到:

ipv6信息

那么这时候你可以在同样有ipv6地址的设备上访问http://[ipv6地址]:5001,其中,ipv6后面的/128或者/64之类的删去。

这个时候如果你成功地打开了DSM的登陆界面,那么恭喜你,你可以进行下一步了。

绑定域名

直接使用ip地址连接可能存在两个问题,一是ipv6地址太长不好记,二是运营商可能定期更换设备分配的地址,因此你可以通过DDNS(动态域名服务)绑定域名来解决这两个麻烦。

DSM的控制面板自带有DDNS功能,但是目前看到只有服务供应商为Synology时才支持ipv6,而我用的黑裙,从未也不想注册群晖账号,所以放弃自带的方式。

搜索ddns+ipv6,网上有很多阿里云的教程,适用于dnspod的少之又少,但我最终找到了一个可用的脚本。

第一步,添加解析;

登陆dnspod控制台,添加一条记录类型为AAAA的记录,记录值填写下面的示例2400:da00::dbf:0:100即可。当然脚本貌似可以根据设置自动添加,我没尝试所以无法判断。

解析ipv6

第二步,添加API 密钥;

访问dnspod控制台API 密钥设置,创建一个dnspod token,然后保存下面的ID 和Token:

dnspod添加密钥

配置脚本

这个脚本来源于GitHub上的分享,作者写的注释非常详细,但是因为中间三个空行导致报错,我删除了空行后面甚至删了一部分基础的注释内容,需要修改的内容仅前面七项,其它不变。

#!/usr/bin/bash    
dnspod_ddnsipv6_id="123456" #【API_id】将引号内容修改为获取的API的ID
dnspod_ddnsipv6_token="xc8f52fs85f51f1f5sf" #【API_token】将引号内容修改为获取的API的token
dnspod_ddnsipv6_ttl="600" # 【ttl时间】解析记录在 DNS 服务器缓存的生存时间,默认600(s/秒)
dnspod_ddnsipv6_domain='hin.cool' #【已注册域名】引号里改成自己注册的域名
dnspod_ddnsipv6_subdomain='test' #【二级域名】将引号内容修改为自己想要的名字
get_ipv6_mode='1' # 【获取IPV6方式】支持两种方式,第一种是直接从你的网卡获取,用这种方法请填1。一种是通过访问网页接口获取公网IP6,这种方法请填2
local_net="eth0" # 【网络适配器】 默认为eth0,如果你的公网ipv6地址不在eth0上,需要修改为对应的网络适配器
if [ "$dnspod_ddnsipv6_record" = "@" ]
then
dnspod_ddnsipv6_name=$dnspod_ddnsipv6_domain
else
dnspod_ddnsipv6_name=$dnspod_ddnsipv6_subdomain.$dnspod_ddnsipv6_domain
fi

die0 () {
echo "IPv6地址提取错误,无ipv6地址或非公网IP(fe80开头的非公网IP)"
exit
}

die1 () {
echo "IPv6地址提取错误,请使用ip addr命令查看自己的网卡中是否有IPv6公网(非fe80开头)地址,若网卡有IPv6地址却无法获取成功,可尝试在脚本中切换第二种模式获取"
exit
}

die2 () {
echo "尝试访问网页http://[2606:4700:4700::1111]/cdn-cgi/trace 查看返回的IPv6地址是否能够正常访问本机,无法访问网页则切换第一种模式获取"
exit
}

if [[ "$get_ipv6_mode" == 1 ]]
then
echo "使用本地网卡获取IPv6"
ipv6_list=`ip addr show $local_net | grep "inet6.*global" | awk '{print $2}' | awk -F"/" '{print $1}'` || die1
else
echo "使用网页接口获取IPv6"
ipv6_list=$(curl -s -g http://[2606:4700:4700::1111]/cdn-cgi/trace | sed -n '3p' ) || die
ipv6_list=${ipv6_list##*=}
fi






for ipv6 in ${ipv6_list[@]}
do
if [[ "$ipv6" =~ ^fe80.* ]]
then
continue
else
echo 获取的IP为: $ipv6 >&1
break
fi
done

if [ "$ipv6" == "" ] || [[ "$ipv6" =~ ^fe80.* ]]
then
die0
fi

dns_server_info=`nslookup -query=AAAA $dnspod_ddnsipv6_name 2>&1`

dns_server_ipv6=`echo "$dns_server_info" | grep 'address ' | awk '{print $NF}'`
if [ "$dns_server_ipv6" = "" ]
then
dns_server_ipv6=`echo "$dns_server_info" | grep 'Address: ' | awk '{print $NF}'`
fi

if [ "$?" -eq "0" ]
then
echo "你的DNS服务器IP: $dns_server_ipv6" >&1

if [ "$ipv6" = "$dns_server_ipv6" ]
then
echo "该地址与DNS服务器相同。" >&1
fi
unset dnspod_ddnsipv6_record_id
else
dnspod_ddnsipv6_record_id="1"
fi

send_request() {
local type="$1"
local data="login_token=$dnspod_ddnsipv6_id,$dnspod_ddnsipv6_token&domain=$dnspod_ddnsipv6_domain&sub_domain=$dnspod_ddnsipv6_subdomain$2"
return_info=`curl -X POST "https://dnsapi.cn/$type" -d "$data" 2> /dev/null`
}

query_recordid() {
send_request "Record.List" ""
}

update_record() {
send_request "Record.Modify" "&record_type=AAAA&record_line=默认&ttl=$dnspod_ddnsipv6_ttl&value=$ipv6&record_id=$dnspod_ddnsipv6_record_id"
}

add_record() {
send_request "Record.Create" "&record_type=AAAA&record_line=默认&ttl=$dnspod_ddnsipv6_ttl&value=$ipv6"
}

if [ "$dnspod_ddnsipv6_record_id" = "" ]
then
echo "解析记录已存在,尝试更新它" >&1
query_recordid
code=`echo $return_info | awk -F \"code\":\" '{print $2}' | awk -F \",\"message\" '{print $1}'`
echo "返回代码: $code" >&1
if [ "$code" = "1" ]
then
dnspod_ddnsipv6_record_id=`echo $return_info | awk -F \"records\":.{\"id\":\" '{print $2}' | awk -F \",\"ttl\" '{print $1}'`
update_record
echo "更新解析成功" >&1
else
echo "错误代码返回,域名不存在,请尝试添加。" >&1
add_record
echo "添加成功" >&1
fi
else
echo "该域名不存在,请在dnspod控制台添加"
add_record
echo "添加成功" >&1
fi

其中,建议使用方式一,因为我看到有博主使用方式二频繁请求所以被网页拉黑了,导致无法更新ipv6地址。

获取网卡名称的方式也很多,比如ssh 登陆之后,输入ifconfig即可查看网卡名称和ip地址等信息。

运行脚本

你可以把脚本上传到群晖任意一个位置,然后属性查看该脚本的路径,比如我的在/volume1/ddns/下面。

运行脚本的方法也很简单,比如在任务计划中新增一个计划的任务,随便输个任务名称。计划里面设置一个运行日期,有的博主设置的十分钟,本来运营商更换地址不可能这么频繁,设置频率高一点可以更快在ipv6地址更改后提交给dnspod。然后在任务设置里,运行命令下面输入你刚才复制的文件路径(要包含文件)。

保存之后可以点击运行,但是,我这里没有成功!添加了通过邮件通知详情才看到:

ddns脚本运行详情

我一度以为是文件路径不对,却总也找不到问题,于是我打算登陆ssh去查看一下详情。

在ssh登陆下看到,文件确实存在,路径也没错,于是我尝试使用命令运行。cd到脚本目录,然后输入bash ddns.sh

这个时候具体的报错便出现了,见下图红框。于是我删掉代码原本13-15行的空格,保存,重新上传。

但是在30行还有一个报错:

syntax error near unexpected token `$’{\r’'

在网上找到原因和解决办法,原因是文件在Windows上复制的,复制之后,文件的换行符为dos格式的\r\n而Linux则为unix格式的\n,所以提示该语法错误。

运行sed -i 's/\r//g' ddns.sh,再运行脚本,可以看到输出结果提示成功:

命令运行脚本

添加定时任务

因为我们不知道ip地址什么时候变化,所以得依靠定时任务来触发检测。上部分提到了添加任务计划的方法,并且我再次手动运行了该计划,也收到了邮件提醒我成功。

但是在设置定时任务时要注意,默认的修改了时间之后,最后运行时间可能是零点以后,所以需要手动调整,确保下次运行时间处的显示符合你的要求。

脚本任务计划

总结

这次的折腾一直从晚上十一点多到凌晨一点半,期间还配置了对应域名的ssl证书。今天为了测试计划任务是否按时执行并且生效,我重启了一下路由器,等待计划设定时间之后查看dnspod的操作日志,发现确实正常把新的ipv6地址提交到了dnspod。也就是说,只要运营商不更改ipv6的策略(比如不提供公网v6),以后在任何地方,只要我的设备同样有ipv6地址,我就可以通过该域名不限速访问群晖了。

dnspod操作日志

顺便提醒一下,把群晖曝到公网环境存在一定安全风险,可以参考我此前写的《谈谈群晖安全》,加固自己的群晖安全配置。