意外地发现新搬进来的公寓里装的电信网有公网 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,可以写成拨号器钩子,还可以放路由器上运行…… 脚本就在这里,具体怎么玩就看个人需求啦。