依托阿里云搭建IPV6DDNS

依托阿里云搭建IPV6DDNS

IPV4+Frp内网穿透与IPV6+DDNS解决方案

搭建私有云盘时由于没有公网ipv4地址, 要想外网访问, 有两种方式:

  1. IPV4 + Frp 内网穿透::
  • 这种方式需要一台有IPV4地址的设备作为Frp服务端, 通过nat方式穿透服务端的一个端口到内网设备的一个端口上, 借助Frp服务端设备的端口对外提供服务.
  • 优点: 提供的是IPV4服务, 几乎所有网络都可以访问
  • 缺点: 受限于Frp服务端的带宽, 对网盘等服务不友好(frp点对点穿透模式除外)
  1. IPV6 + DDNS:
  • 这种方式需要设备使用的网络分配了ipv6地址, 通过ipv6地址直接对外提供服务, 或者域名解析到ipv6地址对面提供服务.
  • 优点: 没有带宽限制, 没有端口限制, 公网直达
  • 缺点: ipv6网络普及度不高, 部分网络无法访问到目标设备的IPV6地址
4G网络同时支持ipv4和ipv6, 无需担心4G网络访问不到目标设备的ipv6地址, 因此选择IPV6DDNS解决方案

##########################################################

依托阿里云SDK搭建IPV6DDNS

从阿里云文档中心可以查到有关DDNS的SDK以及接口文档
https://help.aliyun.com/document_detail/141482.html?spm=a2c1d.8251892.0.0.427f5b76Q7Repp
搭建DDNS需要三个条件:
域名, AccessKey, SDK
其中域名和AccessKey可以直接申请, SDK可以直接引入maven依赖

  1. 创建一个springboot项目

image.png

  1. 编辑pom文件引入阿里云DDNS SDK
<packaging>jar</packaging>
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.1.RELEASE</version>
	<relativePath/>
</parent>

	<dependencies>
		<!--spring mvc-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core -->
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-core</artifactId>
			<version>4.4.3</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-alidns -->
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-alidns</artifactId>
			<version>2.0.10</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
		<finalName>IPv6DDNSApplication</finalName>
	</build>

  1. 编写启动类,开启定时任务支持
package cn.zack;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

// 开启定时任务支持
@EnableScheduling
@SpringBootApplication
public class IPv6DDNSApplication {
	public static void main(String[] args) {
		SpringApplication.run(IPv6DDNSApplication.class, args);
	}
}
  1. 配置文件
server:
  port: 8080

aliyun:
  ddns:
    regionId: cn-hangzhou
    # 要解析的主域名
    domainName: zack.net.cn
    # 记录值
    keyWord: ipv6.pan
    # 记录类型(AAAA为解析ipv6)
    type: AAAA
    # 令牌及秘钥
    accessKeyId: ----------
    secret: --------------

  1. 编写定时任务, 指定执行时间
package cn.zack.schedule;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.alidns.model.v20150109.DescribeDomainRecordsRequest;
import com.aliyuncs.alidns.model.v20150109.DescribeDomainRecordsResponse;
import com.aliyuncs.alidns.model.v20150109.UpdateDomainRecordRequest;
import com.aliyuncs.alidns.model.v20150109.UpdateDomainRecordResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;

@Service
public class IPv6DDNSScheduled {

    @Value("${aliyun.ddns.regionId}")
    private String regionId;

    // 要解析的主域名
    @Value("${aliyun.ddns.domainName}")
    private String domainName;

    // 记录值
    @Value("${aliyun.ddns.keyWord}")
    private String keyWord;

    // 记录类型
    @Value("${aliyun.ddns.type}")
    private String type;

    // accesskeyId
    @Value("${aliyun.ddns.accessKeyId}")
    private String accessKeyId;

    // secret
    @Value("${aliyun.ddns.secret}")
    private String secret;

    /**
     * 定时执行ipv6DNS, 每10分钟一次
     *
     * @throws SocketException
     */
    @Scheduled(fixedRate = 6000 * 10 * 10)
    public void autoChangeIPv6DNS() throws SocketException {
        System.out.println(new Date() + "开始更新IPV6DNS信息====================");
        // 设置主机地址以及AccessKey
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, secret);
        IAcsClient client = new DefaultAcsClient(profile);
        IPv6DDNSScheduled ddnsTestApplication = new IPv6DDNSScheduled();

        // 查询二级域名的最新解析记录
        DescribeDomainRecordsRequest describeDomainRecordsRequest = new DescribeDomainRecordsRequest();
        // 主域名
        describeDomainRecordsRequest.setDomainName(domainName);
        // 主机记录
        describeDomainRecordsRequest.setRRKeyWord(keyWord);
        // 解析记录类型
        describeDomainRecordsRequest.setType(type);
        // 获取主域名所有符合条件的解析记录
        DescribeDomainRecordsResponse describeDomainRecordsResponse = ddnsTestApplication.describeDomainRecords(describeDomainRecordsRequest, client);
        System.out.println("当前主域名的解析记录: \n" + new Gson().toJson(describeDomainRecordsResponse));
        List<DescribeDomainRecordsResponse.Record> domainRecords = describeDomainRecordsResponse.getDomainRecords();
        // 取最新的一条解析记录
        if (domainRecords.size() != 0) {
            DescribeDomainRecordsResponse.Record record = domainRecords.get(0);
            // 取记录Id
            String recordId = record.getRecordId();
            // 取记录值
            String recordValue = record.getValue();
            // 取当前主机的公网ipv6地址
            String iPv6Address = ddnsTestApplication.getIPv6Address();
            System.out.println("当前主机的ipv6地址:" + iPv6Address);
            // 判断是否取到ipv6地址, 取不到就改为和记录值相同, 不会修改解析记录
            if (iPv6Address == "") {
                System.out.println("获取当前主机的ipv6地址失败====================");
                iPv6Address = recordValue;
            }
            // 当前记录值与当前ipv6地址值不一致
            if (!iPv6Address.equals(recordValue)) {
                // 修改解析记录
                UpdateDomainRecordRequest updateDomainRecordRequest = new UpdateDomainRecordRequest();
                // 主机记录
                updateDomainRecordRequest.setRR(keyWord);
                // 记录ip
                updateDomainRecordRequest.setRecordId(recordId);
                // 记录值
                updateDomainRecordRequest.setValue(iPv6Address);
                // 解析记录类型
                updateDomainRecordRequest.setType(type);
                // 进行修改
                UpdateDomainRecordResponse updateDomainRecordResponse = ddnsTestApplication.updateDomainRecord(updateDomainRecordRequest, client);
                System.out.println("解析记录修改结果: \n" + new Gson().toJson(updateDomainRecordResponse));
                System.out.println(new Date() + "IPV6DNS信息更新完成====================");
            } else {
                System.out.println(new Date() + "无需更新IPV6DNS解析记录====================");
            }
        }
    }

    /**
     * 获取主域名的所有解析记录
     *
     * @param request
     * @param client
     * @return
     */
    private DescribeDomainRecordsResponse describeDomainRecords(DescribeDomainRecordsRequest request, IAcsClient client) {
        try {
            // 调用SDK发送请求
            return client.getAcsResponse(request);
        } catch (ClientException e) {
            e.printStackTrace();
            // 调用发生错误
            throw new RuntimeException();
        }
    }

    /**
     * 修改解析记录
     *
     * @param request
     * @param client
     * @return
     */
    private UpdateDomainRecordResponse updateDomainRecord(UpdateDomainRecordRequest request, IAcsClient client) {
        try {
            // 调用SDK发送请求
            return client.getAcsResponse(request);
        } catch (ClientException e) {
            e.printStackTrace();
            // 调用发生错误
            throw new RuntimeException();
        }
    }

    /**
     * 获取本机公网ipv6地址
     *
     * @return
     * @throws SocketException
     */
    private static String getIPv6Address() throws SocketException {
        String ipv6Address = "";
        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
            // 取所有网卡信息进行遍历
            while (addresses.hasMoreElements()) {
                InetAddress in = addresses.nextElement();
                // 取每个网卡的ipv6信息
                if (in instanceof Inet6Address) {
                    // 取ipv6地址
                    String thisIPv6Address = in.getHostAddress();
                    System.out.println(thisIPv6Address);
                    // 有线网卡分配的公网ipv6地址格式为2408:8220:9514:5870:3288:5a2e:6c5b:4d53%eth0
                    // 无线网卡分配的公网ipv6地址格式为2408:8220:9514:5870:d362:977f:a982:5c16%wlan0
                    // 此处只取有线网卡
                    if (thisIPv6Address.startsWith("2") && thisIPv6Address.endsWith("%eth0")) {
                        // 去除ipv6地址附带的网卡信息
                        ipv6Address = thisIPv6Address.split("%")[0];
                        break;
                    }
                }
            }
        }
        return ipv6Address;
    }
}

  1. IPV6 DDNS效果

image.png

##########################################################

将应用设为开机自启

以Debian系列为例, 思路为先创建启动脚本, 然后将启动脚本添加到开机自启项或者系统服务中

  1. 创建启动脚本
root@raspberrypi:/java/ddns# vim ddnsservice.sh
#!/bin/bash
# 脚本内容, 以&结尾,允许在控制台输出日志信息
java -jar IPv6DDNSApplication.jar &
  1. 测试脚本
# 先给脚本赋予可执行权限
root@raspberrypi:/java/ddns# chmod +x ddnsservice.sh
# 执行脚本
root@raspberrypi:/java/ddns# ./ddnsservice.sh
  1. 脚本执行效果

image.png

  1. 添加为系统服务并开机自启
# 先在/etc/systemed/system目录下创建一个自定义服务ddns.service
root@raspberrypi:/etc/systemed/system# vim ddns.service
# ddns.service内容
[Unit]
Description=IPv6DDNSApplication Service
After=network.target

[Service]
Type=forking
# 脚本启动路径
ExecStart=/java/ddns/ddnsservice.sh
ExecStop=/bin/kill -s QUIT $MAINPID

[Install]
WantedBy=multi-user.target
# 修改ddns.service为可执行权限
root@raspberrypi:/etc/systemed/system# chmod +x ddns.service
# 改为自启服务
root@raspberrypi:/etc/systemed/system# systemctl enable ddns.service
  1. 重启查看服务启动情况

查看命令systemctl status ddns.service
image.png