Go语言(GIN)搭建DDNS服务器

天锦 发表于 码农也得有格调 分类,标签:

最近由于异地组网的事情,节点都是动态的IP,在使用阿里云的API做DDNS时,由于阿里云的限制,最小的TTL也只能设置到600,IP更新一次就要折腾至少10分钟,期间就会造成长时间的断联,而且我内部的Kubernetes等系统又要使用基于nsupdate(RFC 2136中的动态更新功能)来更新记录,所以自建一个标准的基于RFC 2136定义的DDNS系统迫在眉睫!此外还要提供一个HTTP接口用以路由器设备使用curl来更新IP。

总体架构

首先使用bind服务作为DNS服务器,配置成允许使用nsupdate工具动态更新,完成兼容RFC 2136协议的DDNS系统,然后再使用web语言来实现这个http接口。再网上搜集资料经常看到的都是基于php的,但是如果使用php的话就要使用exec或者system这种高风险的函数来调用nsupdate,一般为了安全起见,管理员通常是将这些函数都是禁用掉的,所以最后选用的是比较适合做后端的go语言,也就是gin框架来做这个HTTP的API。

搭建DNS

[root@centos ~]# yum install bind bind-utils -y
[root@centos ~]# systemctl enable named.service --now
[root@centos ~]# vim /etc/named.conf

配置内容如下

options {
        listen-on port 53 { any; };
        listen-on-v6 port 53 { ::1; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";
        recursing-file  "/var/named/data/named.recursing";
        secroots-file   "/var/named/data/named.secroots";
        allow-query     { any; };
        /* 
         - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion.
         - If you are building a RECURSIVE (caching) DNS server, you need to enable 
           recursion. 
         - If your recursive DNS server has a public IP address, you MUST enable access 
           control to limit queries to your legitimate users. Failing to do so will
           cause your server to become part of large scale DNS amplification 
           attacks. Implementing BCP38 within your network would greatly
           reduce such attack surface 
        */
        recursion yes;
        dnssec-enable yes;
        dnssec-validation yes;
        /* Path to ISC DLV key */
        bindkeys-file "/etc/named.root.key";
        managed-keys-directory "/var/named/dynamic";
        pid-file "/run/named/named.pid";
        session-keyfile "/run/named/session.key";
};
logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};
zone "." IN {
        type hint;
        file "named.ca";
};
include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

也就是改listen-on port 53 为any,使bind服务监听所有接口的53端口,以及 allow-query为any,允许任何来源的请求,最后一行include "/etc/named.rfc1912.zones"则包含的我们自己的域的配置文件,编辑/etc/named.rfc1912.zones,添加自己的域

zone "ddns.mydomain.com" IN {
        type master;
        file "ddns.mydomain.com";
        allow-update { none; };
};

这里file则是用来保存对应域名下的记录信息的,从/var/named/named.localhost复制一份到ddns.mydomain.com,再打开编辑它。

[root@centos ~]# cp /var/named/named.localhost /var/named/ddns.mydomain.com
[root@centos ~]# vim /var/named/ddns.mydomain.com

可以看到有这些内容,把$TTL的值又默认的1D改为1,也就是缓存生命周期是1秒,这样就能让全网的解析都是实时的设备端IP。

编辑下面的记录就可以进行对应主机名的解析了。(公网解析记得把域名的ns记录指向这台DNS服务器,修改记录文件要生效需要重启named服务)

$TTL 1
@    IN SOA    @ rname.invalid. (
    0; serial
    1D; refresh
    1H; retry
    1W; expire
    3H ); minimum
NS @
A    127.0.0.1
AAAA    ::1
client    A    1.1.1.1

动态解析

要实现动态解析,则需要修改/etc/named.rfc1912.zones文件中对应域名的allow-update权限,可以是基于key的,也可以是基于主机地址的,比如允许服务器本机执行nsupdate进行更新。使用key更新,则需要生成一个key:

[root@centos ~]# dnssec-keygen -a HMAC-MD5 -b 128 -n HOST client.ddns.mydomain.com
[root@centos ~]# cat Kclient.ddns.mydomain.com.+157+56449.private
Private-key-format: v1.3
Algorithm: 157 (HMAC_MD5)
Key: WzVS385ZPCQl0bj5//w/ig==
Bits: AAA=
Created: 20211011151338
Publish: 20211011151338
Activate: 20211011151338

记住这个Key WzVS385ZPCQl0bj5//w/ig==,编辑named.rfc1912.zones,内容如下:

key "DdnsUpdateKey" {
        algorithm hmac-md5;
        secret "WzVS385ZPCQl0bj5//w/ig==";
};
zone "ddns.mydomain.com" IN {
        type master;
        file "ddns.mydomain.com";
        allow-update { key DdnsUpdateKey;localhost; };
};

这样一来,一个标准的DDNS服务器就搭建完成了,可以使用nsupdate试一下了,在服务器本机上可以直接update,在其他主机上则需要另行指定Key进行update

[root@centos ~]# nsupdate
> server 127.0.0.1
> zone ddns.mydomain.com
> update delete client.ddns.mydomain.com A
> update add client.ddns.mydomain.com 10 A 2.2.2.2
> send

HTTP更新接口

网上有很多提供DDNS服务的服务器,不同的路由器厂商支持的又不尽相同,但是大多是基于HTTP接口,使用CURL来进行更新的,以3322为例,他的接口是

http://username:password@members.3322.org/dyndns/update?system=dyndns&hostname=<host>&myip=<address>

实现起来也很简单,搭好gin框架,做好/dyndns/update的路由,hostname和IP地址是通过get传参传入服务器,用户认证则是使用HTTP BasicAuth进行的认证,难点在BasicAuth和调用nsupdate进行更新。

获取BasicAuth信息的代码是

baseAuth := c.Request.Header.Get("Authorization")

具体如何认证就看你实际情况了。

至于调用nsupdate更新的问题,因为nsupdate是一个console控制台,他不是普通命令行那样参数代进去执行一下就行了,而是会开启一个新的虚拟控制台,你需要在这个新的虚拟控制台中输入update信息进行更新。不过我研究出来一个对应的方法,就是使用echo和管道组合起来。编辑public-update.sh

#!/bin/bash
./public-nsupdate.sh $1 $2 | nsupdate

编辑public-nsupdate.sh

#!/bin/bash
echo "server 127.0.0.1"
echo "zone ddns.mydomain.com"
echo "update delete $1 A"
echo "update add $1 10 A $2"
echo "send"
echo "quit"

这样一来,执行public-update.sh fqdn ip就能进行更新了

[root@centos ~]# ./public-update.sh client.ddns.mydomain.com 3.3.3.3

在gin中调用它:

cmd := exec.Command("/bin/bash","-c", "/your-path/public-update.sh " + domain + " " + ip)
updateResult,updateErr:=cmd.Output()
fmt.Println(string(updateResult))

完!