接着上篇《使用 Nginx 提供 DDNS 服务(前篇)》继续聊聊如何玩转 Nginx 和 NJS,本篇将基于上一篇的内容,调整架构,让这套服务能够在云端运行,降低本地调用成本。

本篇文章中,我们实际使用的代码行数会比上篇文章更少,全部代码 150 行左右。

写在前面

和上篇文章一样,我们需要先了解本篇内容中的服务架构和工作流程,为了便于理解,我画了一个简单的流程图。

抽象 DDNS 工作流程

如果你是 NAS (如:群晖)或者向日葵这类软件服务用户,你会发现图中的模式和你之前使用的模式几乎一摸一样。

在这个方案中,我们本地不再需要运行容器或者 Nginx 实例,在路由器或者 NAS 中运行一个计划任务,使用 Curl 之类的方式定时调用在云服务器上部署的服务接口,即可完成 DDNS 记录更新,甚至你在家用电脑上打开网站,设置页面自动刷新也可以达到同样的效果。

相比较方案一,这个方案对于设备要求更低一些,至于使用哪一种,根据自己手里设备资源状况来确定就好啦。

那么,我们就来展开聊聊,怎么通过 Nginx 和容器完成这个服务方案。

使用 Nginx 完成 IP 获取逻辑

我们还是使用 Nginx 先来完成 IP 获取逻辑,这里我们有两个选择,一个是和前文一样,使用外部服务来完成 IP 查询逻辑,还有一个选择便是直接使用 Nginx 来高效的完成这个功能。

因为部署在云端,获取 IP 和 DNS 记录更新逻辑可以合并在一起,但是为了方便理解,这里将两部分拆解开来进行描述。

常规和一般容器方案

如果你在云服务器上通过 APTYUM 安装 Nginx ,那么直接使用下面的配置启动 Nginx ,就能够将访问者的 IP 展示出来啦。

server {
    listen 80;
    server_name localhost;
    charset utf-8;

    location / {
        default_type text/plain;
        return 200 "$remote_addr";
    }
}

当然,为了维护更简单,推荐使用容器来启动服务,将上面的配置保存为 nginx.conf ,然后编写编排文件:

version: "3"
services:

  ngx-whats-myip:
    image: nginx:1.21.1-alpine
    volumes:
      - ./nginx.conf:/etc/nginx/templates/default.conf.template:ro
    ports:
      - 80:80
    environment:
      - NGINX_ENTRYPOINT_QUIET_LOGS=1

然后,将上面的内容保存为 docker-compose.yml ,使用 docker-compose up -d 启动服务,访问服务器 IP 和你指定的端口,一个属于你自己的私有的查询 IP 的服务就就绪啦。

如果你是我的老读者,我更推荐你使用 Traefik 进行维护管理。

Traefik 方案

使用 Traefik 可以让你更轻松的管理服务域名,进行动态快速的服务发现,但是因为要经过 Traefik 这个网关,所以我们需要进行一些配置调整,才能够让服务正常运行。

先对 Nginx 配置文件进行调整:

server {
    listen 80;
    server_name localhost;
    charset utf-8;

    set_real_ip_from 172.160.0.0/16;
    set_real_ip_from 172.170.0.0/16;
    set_real_ip_from 172.180.0.0/16;
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    location / {
        default_type text/plain;
        return 200 "$remote_addr";
    }
}

可以看到我这里使用 set_real_ip_from 设置了三个信任的网络环境,这些数值是怎么来的呢?很简单,使用 docker info,可以看到输出信息最下面有类似这样的信息:

...
...
 Live Restore Enabled: false
 Default Address Pools:
   Base: 172.160.0.0/16, Size: 24
   Base: 172.170.0.0/16, Size: 24
   Base: 172.180.0.0/16, Size: 24

这里你有几个地址,就将几个地址填充到配置里即可。此外,容器编排文件中添加 Traefik 声明即可:

version: "3"
services:

  ngx-ip:
    image: nginx:1.21.1-alpine
    volumes:
      - ./nginx.conf:/etc/nginx/templates/default.conf.template:ro
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"

      - "traefik.http.routers.ngx-whatsmyip-www.entrypoints=http"
      - "traefik.http.routers.ngx-whatsmyip-www.rule=Host(`whatsmyip.lab.io`)"
      - "traefik.http.routers.ngx-whatsmyip-ssl.entrypoints=https"
      - "traefik.http.routers.ngx-whatsmyip-ssl.tls=true"
      - "traefik.http.routers.ngx-whatsmyip-ssl.rule=Host(`whatsmyip.lab.io`)"

      - "traefik.http.services.ngx-whatsmyip-backend.loadbalancer.server.scheme=http"
      - "traefik.http.services.ngx-whatsmyip-backend.loadbalancer.server.port=80"

networks:
  traefik:
    external: true

关于 Traefik 的使用,可以参考之前的文章,如果你没有使用过服务发现,那么它会打开你新世界的大门。

当然,如果你还是希望使用外部服务,也可以继续使用公网 IP 查询服务。关于公网 IP 查询服务,文章末尾有聊,感兴趣的朋友可以自取。

调整 DNS 注册服务

在上一篇文章中,我们有提到可以使用健康检查来完成类似计划任务的功能来进行周期性的 DNS 记录更新。在这个场景下,我们需要进行一些调整。

先来调整 NJS 逻辑,相比较之前需要实现一个 whatsMyIP 来获取外部 IP 地址,这次我们可以通过 r.remoteAddress 属性字段简单的获取 IP。

function main(r) {
    const clientIP = r.remoteAddress;
    const domain = recordName;
    getRecordIds(r, zoneId, domain).then(recordId => {

        if (recordId) {
            updateExistRecord(r, zoneId, domain, recordId, clientIP).then(response => {
                r.return(200, response);
            }).catch(e => r.return(500, e));

        } else {
            createRecordByName(r, zoneId, domain, clientIP).then(response => {
                r.return(200, response);
            }).catch(e => r.return(500, e));
        }

    }).catch(e => r.return(500, e));
}


export default { main }

Nginx 参考前文,也可以进行一些简单的调整。

load_module modules/ngx_http_js_module.so;

user nginx;
worker_processes auto;

error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;


events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    keepalive_timeout 65;
    gzip on;

    js_path "/etc/nginx/njs/";
    js_import app from app.js;

    server {
        listen 80;
        server_name localhost;

        charset utf-8;
        gzip on;
        set_real_ip_from 172.160.0.0/16;
        set_real_ip_from 172.170.0.0/16;
        set_real_ip_from 172.180.0.0/16;
        real_ip_header X-Forwarded-For;
        real_ip_recursive on;

        # Bind request to CF
        location /client/v4/ {
            internal;
            gunzip on;
            proxy_set_header "X-Auth-Email" "${DNS_CF_USER}";
            proxy_set_header "X-Auth-Key"   "${DNS_CF_TOKEN}";
            proxy_set_header "Content-Type" "application/json";
            proxy_pass "https://api.cloudflare.com/client/v4/";
        }

        location / {
            default_type text/plain;
            js_content app.main;
        }

    }
}

可以看到,因为私有化部署,这次服务的代码实现要比之前更精简一些。

因为调用方式发生改变,前文中我们使用健康检查定期调用注册更新接口的方式不能使用了,所以我们要单独创建一个接口地址,让容器进行调用,确保服务稳定。

...

    location = /health {
        default_type text/plain;
        access_log off;
        return 200 'alive';
    }

...

当然,编排文件中对应的检查地址也需要进行更新:

...
healthcheck:
  test: ["CMD", "curl", "--silent", "--fail", "http://localhost/health"]
  interval: 5s
  timeout: 5s
  retries: 3
...

使用 Traefik 针对服务进行频率限制

因为不同服务商的接口都存在一定的调用限制,除了像之前文章一样,在调用的时候进行频率限制外,还可以在服务接口处进行调用频率限制,比如下面的配置就限制每个来源每分钟限制调用 10 次。

labels:
  - "traefik.enable=true"
  - "traefik.docker.network=traefik"

  - "traefik.http.middlewares.test-ratelimit.ratelimit.average=10"
  - "traefik.http.middlewares.test-ratelimit.ratelimit.burst=1"
  - "traefik.http.middlewares.test-ratelimit.ratelimit.period=1m"

如果你希望添加鉴权,进一步减少公开调用,可以参考之前的文章《Traefik 2 基础授权验证(前篇)》进行配置。

补充公网 IP 查询服务

公网上能够做到 IP 查询的服务很多,上篇文章中,我们使用的是自 2010 年运行至今的 SOHU 打点接口,稳定性还是比较有保障的。如果你希望使用更中立的服务商,可以考虑 IPIP 的服务。

在上篇文章发布后,国内专业的 IP 地址库产品 IPIP 的创始人,高春辉大叔留言提醒 IPIP 也有免费的 IP 自查服务。

使用方法也很简单,只需要参考下面的配置,更新之前的配置即可:

server {
    listen 80;
    server_name localhost;

    # Bind request to ipip.net
    location /proxy/myip {
        proxy_pass "http://myip.ipip.net/s";
    }
}

说起这个服务,还有一个小细节,不论是使用 Nginx 反向代理的是 HTTP 协议还是 HTTPS 协议,在不配置 gunzip 的情况下,你会发现都可以正常访问。这里或许是作者的小细节,为了照顾新手以及方便调用者,在使用 CDN 保护接口的同时,特别关闭掉了数据压缩。我做了一个小测试,针对 IP 类返回结果,开启压缩之少可以节约 30~40% 的流量。

其他厂商的公网接口

除此之外,如果你希望多使用几条线路作为“备份”,还可以使用下面的接口服务:

# 百度的服务
http://157.255.77.27/v4/resolve
http://180.76.76.200/v4/resolve
http://110.242.69.192/v4/resolve
http://112.80.248.65/v4/resolve
# 美团的服务
http://portal-portm.meituan.com/sully/v2/native/api/getSourceCityCdnList
# 知乎的服务
http://118.89.204.198/resolv?host=zhihu.com

最后

完整代码我已经更新到了 https://github.com/soulteary/njs-ddns-service,如果你有需要,可以自取并进行适当修改。

下一篇 Nginx DDNS 的文章,我将会介绍如何进行完全私有化部署和使用。

–EOF