IPv4/IPv6 双栈 ddns (DNSPod版)

欢迎转载,但请在开头或结尾注明原文出处【blog.chaosjohn.com】

前言

笔者很早以前就有一个梦想,即:我所有的联网设备,都要能随时随地访问到。比如在公司访问我家里的电脑和 NAS,以及在家里访问公司的工作电脑。

于是笔者开始了漫长的折腾之路:

  1. 最开始,家里和公司都是电信宽带,我就不厌其烦地骚扰电信客服,申请公网IP。当路由器分配到了公网IP后,配合 DMZ端口映射,就能将路由器下设备的某些端口暴露在公网上。缺点:如果设备和需要暴露到公网的服务比较多,不拿个记事本把 什么服务暴露在什么端口 记录下来,用到的时候总是会记混淆。
  2. 后来搬家后宽带从电信换成了移动,获取公网IP就成了奢望(我还真上过工信部投诉,结果移动致电过来表示可以给我安排停服退款,我怂了,继续用吧)。然后就买了一个花生棒,即花生壳的内网穿透版,因免费版只给两条映射,所以笔者只能贡献给常用机的 SSH(22端口) 和 VNC(5900端口)。缺点:奈何能用的端口只有两个,以及每月只有2G的流量,笔者撑了一段时间还是放弃了它。
  3. 后来逐渐接触到各种内网穿透方案,先是 ngork,后来是 frp,将服务端部署在了自己的云主机上后,端口数量限制再也不是问题了,想配多少就配多少。缺点:国内云主机的带宽实在太贵,以阿里云为例,1M带宽23元每月,2M带宽46元,3M带宽71元,4M带宽96元,5M带宽125元,带宽越大单价越贵;国外云主机带宽虽然足,但是延迟太高了,真的很难取舍。
  4. 再后来,又接触到了 n2n,个人的云主机上搭建一个节点服务器 supernode,不同设备都连接 supernode,协商后直接进行点对点(P2P)通信,即真正的设备间数据传输是直连的而不经过 supernode。优点:全端口可用,速度不受限。缺点:因为是 P2P 方案,所以需要设备都安装客户端,但它把 Windows / Linux / macOS / Android 都支持遍了,就是没有 iOS 的客户端。
  5. 17年下半年的时候,笔者突然发现了神器 ZeroTier。它就是一个增强版的 n2n,不同的是,它默认使用官方的节点服务器(当然个人也可以自建),更为重要的是,几乎所有的系统平台,它都支持,就连各类NAS也能安装使用。笔者直到今天,还在深度使用它。举个例子证明笔者有多爱它:官网是 zerotier.com,笔者于 2018-01-12 在腾讯云注册了域名 zerotier.cn,专门用来解析 ZeroTier 分配给设备的 内网IP,并续费至今。(如果用 whois 查一下该域名,还能发现笔者的真名和邮箱哦哈哈哈哈哈)缺点:在设备点对点之间的线路优化好之前,丢包比较严重,甚至连不上。但只要线路逐渐优化好之后,速度几乎能跑满带宽。
  6. 19年下旬的时候,笔者突然发现,家庭宽带和手机蜂窝网络,居然都原生支持 IPv6 了,即无论是路由器下的设备,还是插了SIM卡的移动设备,都能分配到 IPv6地址 了。这意味着,通过某设备的 IPv6 地址,可以直接访问到该设备的所有端口(80/443端口是否被禁得看各地运营商政策)。

IPv6 的问题

IPv6 实现所有设备的连通,这点非常棒,就只差一个问题需要解决:适用于 IPv6ddns 方案。

什么是 ddns 呢?它的全称为 dynamic dns,即 动态域名解析

使用场景:家庭宽带下,IP地址 会被运营商定期更换,在自身 IP地址 发生变化后,将新的IP地址提交给 dns解析服务商,让约定的域名解析更新为新的IP地址。

上面 折腾之路[1],通过电信宽带分配的公网IP访问服务,也是需要用 ddns 来更新 IP地址 到自己的域名上,不过一般路由器都内置了 ddns 功能,傻瓜式配置起来也很简单。

但是:

  • 这些成熟且内置的 ddns 方案,都只适用于 IPv4
  • 对于 IPv4,局域网内只需有一台设备(可以是路由器本身,也可以是下属设备)配置 ddns 就可以了,因为整个局域网都共用一个公网IP;而对于 IPv6,每个设备都有自己的公网IP,即需要公网被访问的每台设备都要单独配置 ddns

解决 IPv6ddns

因笔者的域名几乎都是在 腾讯云 上购买的,并且使用 DNSPod 进行解析,故以下所有的方案都是聚焦于 DNSPod

第一版方案

去 DNSPod 开源社区寻找 IPv6-ddns 的解决方案。不得不说,很多工具都写的很棒,代码写的很漂亮,但是都不适用笔者,主要原因为,笔者的设备太多了,所搭载的系统也太杂了,导致那些工具即使为了提高兼容性,采用 纯Python纯Shell 编写,也无法满足所有设备,比如 ESXi,这家伙虽然底层是 Linux,但是魔改了很多,无论是 Shell 还是 Python 环境,都是阉割过的,实测大部分工具都无法正常运行;另一方面,如果遇到多网卡(含虚拟网卡)的场景,那些工具很难获取到准确的且公网可达的本地 IPv6地址

第二版方案

为了解决 第一版方案 的两个痛点:

  1. 如何获取到准确的 IPv6 地址
  2. 如何能在 几乎 所有系统上都适用(除开Windows,因为笔者几乎不用)

针对 痛点1:既然通过读取网卡信息无法做到准确地获取公网可达的 IPv6地址,那就更换思路,网上不是有很多查询自身 IPv6地址 吗,比如 api6.ipify.org

$ curl api6.ipify.org
240e:47b:1660:4939:881f:1234:5678:9abc

因为几乎所有的系统都预装了 curl 或者 wget(比如 ESXicurl 但内置了阉割版的 wget),所以 痛点1 完美解决。

同时也给解决 痛点2 提供了思路:如果我在自己的云主机上部署一个服务,设备定期将自己的 IPv6地址 和与其绑定的域名通过 curl / wget 提交给该服务,让该服务在云主机上代理进行 ddns,将新解析提交给 DNSPod,不就解决了吗?

于是笔者:

  1. 参考 DNSPod APINodeJS 写了一个 ddns代理 的服务部署在云主机上(用 NodeJS 进行编写的原因仅仅是当时的工作内容和 NodeJS 相关,顺手而已)
  2. 各设备上用 cronwhile + sleep 定时(笔者设定的是1分钟)先从 api6.ipify.org 获取本机 IPv6地址 再向该 ddns代理服务 提交。

Perfect!

第三版方案

第二版 方案用的挺舒心的,但是运行了一段时间发现,出问题了,我好多设备都每分钟请求一次,还挺频繁的,导致 api6.ipify.org 限流,拒绝返回。

换用其他的 IPv6地址查询服务,也都一样,隔一段时间就可能被限流。

所以笔者萌生了自己写一个 IPv6地址查询服务,只为自己提供服务,限不限流我自己说了算!

这回为了造轮子能造的快点,放弃了 NodeJS 改用了 PHP,直接上代码:

PHP 代码

$ cat /home/chaos/IPChecker/index.php
<?php

echo $_SERVER['REMOTE_ADDR'];

Nginx 配置文件

$ cat /etc/nginx/conf.d/ip-check.conf
server {
  server_name ip.example.com ipv6.example.com;
  listen 80;
  listen [::]:80;

  root /home/chaos/IPChecker;
  location / {
    try_files $uri $uri/ /index.php;
    fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
  }
}

是不是很简单,PHP 代码就只有一行代码 echo $_SERVER['REMOTE_ADDR'];,即直接输出请求的来源地址,而且同时支持 IPv4/IPv6

  • curl ip.example.com 返回 IPv4 公网地址
  • curl ipv6.example.com 返回 IPv6 公网地址

(注:该服务只能部署在有 IPv6 地址的云主机上,因为 IPv6 只能访问 IPv6

第四版方案

完成一次 ddns,要先从 ipv6.chaosjohn.com 获取 IPv6地址,再向 ddns代理服务 提交,得两步操作,太麻烦了。

所以笔者决定把两项服务合二为一,依旧废话不多说,上代码:

项目根目录

$ ls -alh /home/chaos/ddns-dnspod
total 88K
drwxrwxr-x  4 chaos chaos 4.0K Nov  3 06:57 .
drwxr-xr-x 23 chaos chaos 4.0K Dec 16 15:30 ..
-rw-rw-r--  1 chaos chaos   75 Nov  2 09:24 composer.json
-rw-rw-r--  1 chaos chaos 2.7K Nov  2 09:24 composer.lock
-rw-rw-r--  1 chaos chaos   71 Nov  3 01:46 config.ini
-rw-rw-r--  1 chaos chaos   53 Nov  3 05:54 config-sample.ini
drwxrwxr-x  8 chaos chaos 4.0K Nov  3 06:57 .git
-rw-rw-r--  1 chaos chaos  524 Nov  3 05:57 .gitignore
-rw-rw-r--  1 chaos chaos 2.7K Nov  3 02:59 index.php
-rw-rw-r--  1 chaos chaos  35K Nov  3 06:02 LICENSE
-rw-r--r--  1 chaos chaos  358 Nov  2 14:04 nginx.conf
-rw-r--r--  1 chaos chaos  362 Nov  3 05:53 nginx-sample.conf
-rw-rw-r--  1 chaos chaos  941 Nov  3 06:57 README.md
drwxrwxr-x  4 chaos chaos 4.0K Nov  2 09:24 vendor

配置文件(其中 tokenDNSPod 密钥管理里的 ID,token,形如 30345,ac0000000918368b1cfa16f4fc6e28cd

$ cat /home/chaos/ddns-dnspod/config.ini
[config]
token = 'YOUR-DNSPOD-TOKEN'
key = 'YOUR-KEY'

PHP 代码

$ cat /home/chaos/ddns-dnspod/index.php
<?php

require __DIR__ . '/vendor/autoload.php';

use Curl\Curl;
$curl = new Curl();

$ini_array = parse_ini_file('config.ini', true);

$token = $ini_array['config']['token']; // dnspod token
$required_key = $ini_array['config']['key'];

//echo $_SERVER['HTTP_X_REAL_IP'];
$address = $_SERVER['REMOTE_ADDR'];
$isIPv6 = strpos($address, ':') > -1;
$type = $isIPv6 ? 'AAAA' : 'A';
$domain = $_GET['domain'];
$sub = $_GET['sub'];
$key = $_GET['key'];

if (!$domain || !$sub || 0 != strcmp($key, $required_key)) exit;

$output = [
  'domain' => $domain,
  'sub' => $sub,
  'type' => $type,
  'address' => $address
];

$form = "login_token=${token}&format=json&domain=${domain}&sub_domain=${sub}&record_type=${type}&record_line_id=0";
$response = $curl->post('https://dnsapi.cn/Record.List', $form);
if ($curl->error) {
  $output['msg'] = 'Record.List Error' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
} else {
  $records = $response->records;  
  if (null == $records) $records = [];
  $records = array_filter($records, function($record) use($type) { 
    return 0 == strcmp($record->type, $type); 
  });
  
  if (count($records) > 0) { // if record exists
    $record = $records[0];
    if (0 == strcmp($record->value, $address)) { // skip if same
      $output['msg'] = 'Same record. Skipped';
      echo json_encode($output);
      exit;
    }

    // update record
    $record_id = $record->id;
    $form = "login_token=${token}&format=json&domain=${domain}&record_id=${record_id}&sub_domain=${sub}&value=${address}&record_type=${type}&record_line_id=0";
    $response = $curl->post('https://dnsapi.cn/Record.Modify', $form);
    if ($curl->error) {
      $output['msg'] = 'Record.Modify Error' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
    } else {
      if (null != $response->status && null != $response->status->code && '1' == $response->status->code) {
        $output['msg'] = 'Modification succeeded';
      } else {
        $output = $response;
        $output->action = 'Record.Modify';
      }
    }
  } else { // record not exists => create 
    $form = "login_token=${token}&format=json&domain=${domain}&sub_domain=${sub}&record_type=${type}&record_line_id=0&value=${address}";
    $response = $curl->post('https://dnsapi.cn/Record.Create', $form);
    if ($curl->error) {
      $output['msg'] = 'Record.Create Error' . $curl->errorCode . ': ' . $curl->errorMessage . "\n";
    } else {
      if (null != $response->status && null != $response->status->code && '1' == $response->status->code) {
        $output['msg'] = 'Creation succeeded';
      } else {
        $output = $response;
        $output->action = 'Record.Create';
      }
    }
  }
}
echo json_encode($output, JSON_UNESCAPED_UNICODE);

Nginx 配置文件

$ cat /etc/nginx/conf.d/ddns-dnspod.conf
server {
  server_name ddns.example.com ddns6.example.com;
  listen 80;
  listen [::]:80;

  root /home/chaos/ddns-dnspod;
  location / {
    try_files $uri $uri/ /index.php;
    fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
  }
}

具体使用:

  • cron 的话,在 crontab 里添加一行:
* * * * * logfile='/tmp/ddns6.log'; echo "\n$(date)" >> $logfile && curl 'http://ddns6.example.com?key=YOUR-KEY&domain=DOMAIN&sub=SUBDOMAIN' >> $logfile
  • cron 的话,使用 while + sleep
while true; do logfile='/tmp/ddns6.log'; echo "\n$(date)" >> $logfile && curl 'http://ddns6.example.com?key=YOUR-KEY&domain=DOMAIN&sub=SUBDOMAIN' >> $logfile; sleep 1; done
  • 如果没有 curl,只有 wget 的话,将 curl [url] 替换为 wget -q -O - [url]

代码笔者已经上传到 Github,如何部署请移步 ChaosJohn/ddns-dnspod 参考哈(喜欢的话给个 Star 呗)

最后

所有联网设备都可随时随地访问 这个梦想最终通过 IPv6 完美达成了。

期间经历了多个方案,终于在 折腾之路 上告一段落了。

如果后期有更优解,笔者还会回来的!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容