意外地发现新搬进来的公寓里装的电信网有公网 IP,又可以捣鼓一番咯!这里用 cloudflare 做 DDNS,它的免费 DNS 服务完全够个人使用,API 又完善,脚本写起来很方便。

1. 配置域名

先去注册商把 NS 换到 cloudflare,这不是废话么…… 我有个空闲的域名,就决定用它做 DDNS 啦。随便建个 A 记录后面用,把其他记录都删掉。

2. 获取 API 密钥

在 cloudflare 账户信息页找到 API Key,复制下来保存好后面用。

3. 获取当前 IP

稍微调查了下,从 linux 网卡接口上获取到的 IP 值有可能并不是真正的外网 IP,比如我这的宽带是 PPPoE,NetworkManager 另外用了个虚拟 ppp0 接口来连接网络,从真实网卡上获取的地址并不准确,也可能根本没有地址…… 总体来讲,要从本地获取到真实的外网 IP 既复杂也不能保证正确。

那么换个思路,从外部获知 IP。比如说由公网上的机器传回来真实 IP 值,这就能保证完全正确了!把我以前的消息服务器程序拿过来稍微改改就能成!下面是原来的消息服务器:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define BACK_LOG            10
void* getClientSockAddr(struct sockaddr*);

int main(int argc, char** argv) {
    /* Usage: server <port> <msg> */
    if(argc != 3) {
        fprintf(stderr, "Usage: %s <port> <msg>\n", argv[0]);
        return 1;
    }

    int msgLen = strlen(argv[2]);

    struct addrinfo hints = {};
    struct addrinfo *servAddrInfo;  
    hints.ai_family = AF_UNSPEC;
    hints.ai_flags = AI_PASSIVE;
    hints.ai_socktype = SOCK_STREAM;

    int status = getaddrinfo(NULL, argv[1], &hints, &servAddrInfo);
    if(0 != status) {
        fprintf(stderr, "getaddrinfo(): %s\n", strerror(status));
        return status;
    }

    struct addrinfo *ptr;
    int listenSockfd;
    for(ptr = servAddrInfo;
        ptr != NULL; 
        ptr = ptr->ai_next) {
            listenSockfd = socket(ptr->ai_family, 
                                ptr->ai_socktype, 
                                ptr->ai_protocol);
            int tmp = errno;
            if(-1 == listenSockfd) {
                fprintf(stderr, "socket(): %s\n", strerror(tmp));
                continue;
            }

            int optvalue = 1;
            int status = setsockopt(listenSockfd, 
                                    SOL_SOCKET, 
                                    SO_REUSEADDR, 
                                    &optvalue, 
                                    sizeof(optvalue));
            if(status == -1) {
                int tmp = errno;
                fprintf(stderr, "setsockopt(): %s\n", strerror(tmp));
                close(listenSockfd);
                return tmp;
            }

            status = setsockopt(listenSockfd, 
                                    SOL_SOCKET, 
                                    SO_REUSEPORT, 
                                    &optvalue, 
                                    sizeof(optvalue));
            if(status == -1) {
                int tmp = errno;
                fprintf(stderr, "setsockopt(): %s\n", strerror(tmp));
                close(listenSockfd);
                return tmp;
            }

            status = bind(listenSockfd, ptr->ai_addr, ptr->ai_addrlen);
            if(-1 == status) {
                close(listenSockfd);
                fprintf(stderr, "bind(): %s\n", strerror(status));
                continue;
            }

            break;
    }

    freeaddrinfo(servAddrInfo);
    if(ptr == NULL) {
        fprintf(stderr, "Local address unavailable.\n");
        return 1;
    }

    if(-1 == listen(listenSockfd, BACK_LOG)) {
        int tmp = errno;
        fprintf(stderr, "listen(): %s\n", strerror(tmp));
        return tmp;
    }

    int connSockfd;
    struct sockaddr_storage clientSockAddr = {};
    socklen_t clientSockAddrLen;
    while(1) {
        clientSockAddrLen = sizeof(struct sockaddr_storage);
        connSockfd = accept(listenSockfd, 
                            &clientSockAddr, 
                            &clientSockAddrLen);
        if(connSockfd == -1) 
            continue;

        char* clientAddrStr[INET6_ADDRSTRLEN];
        inet_ntop(clientSockAddr.ss_family,
                  getClientSockAddr((struct sockaddr*)&clientSockAddr),
                  clientAddrStr,
                  INET6_ADDRSTRLEN);
        fprintf(stdout, "accept(): connection from %s\n", clientAddrStr);
        if(send(connSockfd, argv[2], msgLen, 0) == -1) {
            int tmp = errno;
            fprintf(stderr, "send(): %s\n", strerror(tmp));
            close(connSockfd);
            continue;
        }
        close(connSockfd);
    }

    return 0;
}

void* getClientSockAddr(struct sockaddr* client) {
    if(client->sa_family == AF_INET)
        return &(((struct sockaddr_in*)client)->sin_addr);
    else
        return &(((struct sockaddr_in6*)client)->sin6_addr);
}

把发送消息的部分改改,改成发送传入连接的地址:

...
if(send(connSockfd, clientAddrStr, 
        strlen(clientAddrStr), 0) == -1) 
{
    int tmp = errno;
    fprintf(stderr, "send(): %s\n", strerror(tmp));
    close(connSockfd);
    continue;
}
...

当然还有其他地方要改,比如参数判断等等,就不再一一赘述,最终程序源码点此下载

完事后放到公网上运行,比如我在服务器上用它新建个 systemd 服务 ipreporter.service,监听 8081 端口

[Unit]
Description=IP Reporter
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ipreporter 8081

[Install]
WantedBy=default.target

systemctl start ipreporter && systemctl enable ipreporter 立即启动并开机自启,这样用 curl 请求下就能知道本机外网 IP 了(程序写的比较简单,消息发送后就立即关闭 socket 了,curl 请求会有报错,使用 curl -s 忽略错误)。

现在我在电脑上 curl -s everdream.xyz:8081 就可以知道外网 IP 了,可以进一步写进脚本。

4. 使用 cloudflare API

新鲜出炉的 ddns.sh,只更新唯一的 A 记录:

#!/bin/sh
DOMAIN=""         #这里填进你的域名
EMAIL=""        #这里填进cloudflare账户的邮箱
API_KEY=""      #API密钥
#############################
BASE_URL="https://api.cloudflare.com/client/v4/"
BASE_CMD() {
    curl -s \
         -X $1 "$BASE_URL/$2" \
         -H "X-Auth-Email: $EMAIL" \
         -H "X-Auth-Key: $API_KEY" \
         -H "Content-Type: application/json" \
         $3
}
EXTRACT() {
    python -c "import json,sys;print(json.load(sys.stdin)$1)"
}

ZONE_ID=$(
    BASE_CMD "GET" "zones?name=$DOMAIN" |\
    EXTRACT "['result'][0]['id']")
echo "Zone ID: " $ZONE_ID

DNS_RECORD_ID=$(
    BASE_CMD "GET" "zones/$ZONE_ID/dns_records?type=A" |\
    EXTRACT "['result'][0]['id']")
echo "DNS Record ID: " $DNS_RECORD_ID

PUBLIC_IP=$(
    curl -s xcel.me:8081)
echo "Public IP Address: " $PUBLIC_IP

DATA="{\"type\":\"A\",\"name\":\"$DOMAIN\",\"content\":\"$PUBLIC_IP\"}"
RESULT=$(
    BASE_CMD "PUT" "zones/$ZONE_ID/dns_records/$DNS_RECORD_ID" \
             "--data ${DATA}" |\
    EXTRACT "['success']")
echo "Result: " $RESULT

使用 python json 提取消息,简单可靠,不存在跨平台问题。这里是 py3,要用 py2 把 EXTRACT 函数里 print 一对括号去掉就好。运行脚本返回示例,result 为 true 表明 DDNS 更新成功!

Zone ID:            0123456789abcdefg
DNS Record ID:      gfedcba9876543210
Public IP Address:  123.123.123.123
Result:             True

5. 设置 DDNS 脚本自动触发

可以写成 cron 定时运行(cloudflare 每天有上千次请求额度,可以把时间间隔设短一点),可以写成 systemd timer,可以写成拨号器钩子,还可以放路由器上运行…… 脚本就在这里,具体怎么玩就看个人需求啦。