Ali-DDNS

借助阿里云 openapi 实现的 DDNS 服务

GitHub 在新窗口打开 查看 API 在新窗口打开

简介

借助阿里云 openapi 实现的 DDNS 服务

细节

# Architecture

- Server: Obtain domain records from Ali and receive domain name change requests from clients
- Client: Gets the domain record from the server and requests to change the domain record

               +---+    Get Domain Record    +---+
               |   +------------------------>|   |
+------------+ | G |<------------------------+ G | +------------+    +---------+
| Client API | | R |   Return Domain Record  | R | | Server API +--->| Ali API |<------+
+------------+ | P |                         | P | +------+-----+    +---------+       |
               | C +------------------------>| C |        |          +-------+    +----+----+
               |   |   Update Domain Record  |   |        +--------->| Redis +--->| CronJob |
               +---+                         +---+                   +-------+    +---------+
                                                                           ^
               +---+                         +---+                +---+    |
+------------+ | H |  register/login/logout  | H |  grpc-gateway  | G | +--+---------+
|     Web    | | T |<----------------------->| T |<-------------->| R | | Interface  |
+------------+ | T |   add/del Domain Name   | T |                | P | |    API     |
               | P |                         | P |                | C | +------------+
               +---+                         +---+                +---+

如何使用

  • 在客户端和服务端克隆项目:

    git clone --depth=1 https://github.com/hominsu/Ali-DDNS.git
    
  • 客户端服务端 之间的连接是通过 tls 进行身份验证的,因此必须首先创建相关的证书,这里使用 openssl 生成 san 证书

    1. 创建 CA 证书

      ❯ openssl genrsa -out ca.key 4096
      Generating RSA private key, 4096 bit long modulus (2 primes)
      ..................................................................++++
      .........................++++
      e is 65537 (0x010001)
      
      ❯ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
      You are about to be asked to enter information that will be incorporated
      into your certificate request.
      What you are about to enter is what is called a Distinguished Name or a DN.
      There are quite a few fields but you can leave some blank
      For some fields there will be a default value,
      If you enter '.', the field will be left blank.
      -----
      Country Name (2 letter code) [AU]:CN
      State or Province Name (full name) [Some-State]:Guangdong
      Locality Name (eg, city) []:Foshan
      Organization Name (eg, company) [Internet Widgits Pty Ltd]:hominsu
      Organizational Unit Name (eg, section) []:hominsu
      Common Name (e.g. server FQDN or YOUR name) []:localhost
      Email Address []:1774069959@qq.com
      
    2. 准备 openssl 配置文件

      拷贝 openssl 的默认配置文件到当前目录

    • linux:

      cp /etc/pki/tls/openssl.cnf .
      
      
    • macos:

      cp /System/Library/OpenSSL/openssl.cnf .
      

    修改 openssl.cnf 中的以下选项

    • 找到 [CA_default] 并取消 copy_extensions = copy 注释:

      [ CA_default ]
      # Extension copying option: use with caution.
      copy_extensions = copy
      
    • 找到 [ req ] 并取消 req_extensions = v3_req 的注释:

      [ req ]
      req_extensions = v3_req # The extensions to add to a certificate request
      
    • 找到 [ v3_req ] 然后添加 subjectAltName = @alt_names, 然后添加标签 [ alt_names ] 和对应的字段:

      [ v3_req ]       
                       
      # Extensions to add to a certificate request
                       
      basicConstraints = CA:FALSE
      keyUsage = nonRepudiation, digitalSignature, keyEncipherment
      subjectAltName = @alt_names
                       
      [ alt_names ]    
      DNS.1 = localhost
      DNS.2 = *.example.com
      
    1. 生成服务端证书

      ❯ openssl genpkey -algorithm RSA -out server.key
      .....................................................................................................+++++
      ..........+++++
      
      ❯ openssl req -new -nodes -key server.key -out server.csr -days 3650 -config ./openssl.cnf -extensions v3_req
      Ignoring -days; not generating a certificate
      You are about to be asked to enter information that will be incorporated
      into your certificate request.
      What you are about to enter is what is called a Distinguished Name or a DN.
      There are quite a few fields but you can leave some blank
      For some fields there will be a default value,
      If you enter '.', the field will be left blank.
      -----
      Country Name (2 letter code) [AU]:CN
      State or Province Name (full name) [Some-State]:Guangdong
      Locality Name (eg, city) []:Foshan
      Organization Name (eg, company) [Internet Widgits Pty Ltd]:hominsu
      Organizational Unit Name (eg, section) []:hominsu
      Common Name (e.g. server FQDN or YOUR name) []:localhost
      Email Address []:1774069959@qq.com
      
      Please enter the following 'extra' attributes
      to be sent with your certificate request
      A challenge password []:your_password
      An optional company name []:hominsu
      
      ❯ openssl x509 -req -days 3650 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
      Signature ok
      subject=C = CN, ST = Guangdong, L = Foshan, O = hominsu, OU = hominsu, CN = localhost, emailAddress = 1774069959@qq.com
      Getting CA Private Key
      
    2. 生成客户端证书

      ❯ openssl genpkey -algorithm RSA -out client.key
      ..................................................................................+++++
      ..............................................+++++
      
      ❯ openssl req -new -nodes -key client.key -out client.csr -days 3650 -config ./openssl.cnf -extensions v3_req
      Ignoring -days; not generating a certificate
      You are about to be asked to enter information that will be incorporated
      into your certificate request.
      What you are about to enter is what is called a Distinguished Name or a DN.
      There are quite a few fields but you can leave some blank
      For some fields there will be a default value,
      If you enter '.', the field will be left blank.
      -----
      Country Name (2 letter code) [AU]:CN
      State or Province Name (full name) [Some-State]:Guangdong
      Locality Name (eg, city) []:Foshan
      Organization Name (eg, company) [Internet Widgits Pty Ltd]:hominsu
      Organizational Unit Name (eg, section) []:hominsu
      Common Name (e.g. server FQDN or YOUR name) []:localhost
      Email Address []:1774069959@qq.com
      
      Please enter the following 'extra' attributes
      to be sent with your certificate request
      A challenge password []:your_password
      An optional company name []:hominsu
      
      ❯ openssl x509 -req -days 3650 -in client.csr -out client.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
      Signature ok
      subject=C = CN, ST = Guangdong, L = Foshan, O = hominsu, OU = hominsu, CN = localhost, emailAddress = 1774069959@qq.com
      Getting CA Private Key
      

    全部文件如下所示:

    ❯ tree
    .
    ├── ca.crt
    ├── ca.key
    ├── ca.srl
    ├── client.csr
    ├── client.key
    ├── client.pem
    ├── openssl.cnf
    ├── server.csr
    ├── server.key
    └── server.pem
    
    0 directories, 10 files
    

    在服务端 ca.crtserver.pemserver.key 会被用作服务端连接的鉴权,grpc-gateway 会使用 ca.crtclient.pemclient.key 去设置 grpc-gatewaygrpc server 之间的连接的认证

    在客户端,如果你要将服务端提供给别人使员工,为了安全考虑, 你应该为每个客户端使用根证书(ca.crt and ca.key)生成单独的证书,或者你可以直接使用和 gateway 相同的证书(不推荐)

    然后创建一个cert目录,并将证书复制到其中

    在服务端,文件结构如下所示:

    ❯ tree cert
    cert
    ├── ca.crt
    ├── client.key
    ├── client.pem
    ├── server.key
    └── server.pem
    
    0 directories, 5 files
    

    在客户端,文件结构如下所示:

    ❯ tree cert
    cert
    ├── ca.crt
    ├── client.key
    └── client.pem
    
    0 directories, 3 files
    
  • 在服务端,你需要在 docker-compose.yml 中填写你的 Ali Access-Key,设置你的 redis 密码。接口使用 jwt 进行身份验证,你要确保设置了 jwt 令牌(例如 "homing.so")。运营商通常在凌晨更新公网 IP,所以你可以使用Cron表达式(例如: CRON_TZ=Asia/Shanghai 1/10 2-4 * * * )来指定它在清晨以更高的频率更新,而在其他时间以较慢的速度更新(比如每小时更新一次)。你当然可以定义自己的时间。

    redis-ddns:
      image: redis:alpine
      container_name: ali-ddns-redis-ddns
      # 设置 Redis 的密码,下面记得主服务中填写对应的密码
      command: redis-server --port 6380 --requirepass redis-ddns-password
      
    ali-ddns-server-service:
      # 阿里云仓库:  registry.cn-shenzhen.aliyuncs.com/hominsu/ali-ddns-server-service:latest
      # GHCR:      ghcr.io/hominsu/ali-ddns-server-service:latest
      # DockerHub: hominsu/ali-ddns-server-service:latest
      image: hominsu/ali-ddns-server-service:latest
      container_name: ali-ddns-server-service
      # build:
      #   context: .
      #   dockerfile: ./Dockerfile
      depends_on:
        - redis-ddns
      restart: always
      environment:
      # 设置时区,不然 logs 的时间不对
      TZ: "Asia/Shanghai" # 时区
    
      # 设置阿里云的 AK,建议使用 RAM 用户,只分配 AliyunDNSFullAccess 权限
      ALIDDNSSERVER_ACCESSKEY_ID: "*"                                   # 阿里云 AK ID
      ALIDDNSSERVER_ACCESSKEY_SECRET: "*"                               # 阿里云 AK SECRET
    
      ALIDDNSSERVER_BASIC_ENDPOINT: "alidns.cn-shenzhen.aliyuncs.com"   # 阿里云服务地址
      ALIDDNSSERVER_BASIC_INTERFACE_PORT: "50001"                       # WEB 服务监听端口
      ALIDDNSSERVER_BASIC_DOMAIN_GRPC_NETWORK: "tcp"                    # RPC 协议
      ALIDDNSSERVER_BASIC_DOMAIN_GRPC_PORT: "50002"                     # RPC 服务端口
      ALIDDNSSERVER_BASIC_INTERFACE_GRPC_NETWORK: "tcp"                 # RPC 协议
      ALIDDNSSERVER_BASIC_INTERFACE_GRPC_PORT: "50003"                  # RPC 服务端口
    
      # 保存域名相关信息的 Redis,要改的只有密码,和上面设置的密码相同
      ALIDDNSSERVER_DOMAIN_RECORD_REDIS_ADDR: "redis-ddns"              # 保存域名信息的 Redis 地址
      ALIDDNSSERVER_DOMAIN_RECORD_REDIS_PORT: "6380"                    # 保存域名信息的 Redis 端口
      ALIDDNSSERVER_DOMAIN_RECORD_REDIS_PASSWORD: "redis-ddns-password" # 保存域名信息的 Redis 密码
      ALIDDNSSERVER_DOMAIN_RECORD_REDIS_DB: "1"                         # 保存域名信息的 Redis 数据库
      
      ALIDDNSCLIENT_OPTION_JWT_TOKEN: "jwt_token"                       # jwt token
      ALIDDNSCLIENT_OPTION_TTL: "3600"                                  # 每隔多少秒向服务端获取更新信息
      ALIDDNSCLIENT_OPTION_DELAY_CHECK_CRON: "CRON_TZ=Asia/Shanghai 1/10 2-4 * * *" # 每天的 2-4 点的 1m 开始,每 10m 执行一次
    
    
  • 启动服务端

    cd Ali-DDNS/deploy/docker-compose/ddns-server
    docker-compose up -d
    
  • 启动客户端

    cd Ali-DDNS/deploy/docker-compose/ddns-client
    docker-compose up -d
    
  • 设置服务端

    1. 注册用户

      通过 cURL 发送请求:

      curl --location --request POST 'http://127.0.0.1:50001/v1/register' \ 
      --header 'Content-Type: application/json' \
      --data-raw '{
          "username": "admin",
          "password": "passwd"
      }'
      

      或者使用 wget

      wget --no-check-certificate --quiet \
        --method POST \
        --timeout=0 \
        --header 'Content-Type: application/json' \
        --body-data '{
          "username": "admin",
          "password": "passwd"
      }' \
         'http://127.0.0.1:50001/v1/register'
      

      如果请求成功,你会得到以下输出:

      {"status":true}
      
    2. 登陆并获取 token

      cURL:

      curl --location --request POST 'http://127.0.0.1:50001/v1/login' \
      --header 'Content-Type: application/json' \
      --data-raw '{
          "username": "admin",
          "password": "passwd"
      }'
      

      wget:

      wget --no-check-certificate --quiet \
        --method POST \
        --timeout=0 \
        --header 'Content-Type: application/json' \
        --body-data '{
          "username": "admin",
          "password": "passwd"
      }' \
         'http://127.0.0.1:50001/v1/login'
      

      如果请求成功,您将获得以下带有 tokenusername 的输出

      {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ", "username":"admin"}
      
    3. 添加你需要监控的域名, 注意在 url (/v1/{username}/domain_name) 中的 {username} 需要改成你自己的用户名 (例如 admin), token 在上一步中获得

      cURL:

      curl --location --request POST 'http://127.0.0.1:50001/v1/admin/domain_name' \
      --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ' \
      --header 'Content-Type: application/json' \
      --data-raw '{
          "domain_name": "haomingsu.cn"
      }'
      

      wget:

      wget --no-check-certificate --quiet \
        --method POST \
        --timeout=0 \
        --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ' \
        --header 'Content-Type: application/json' \
        --body-data '{
          "domain_name": "haomingsu.cn"
      }' \
         'http://127.0.0.1:50001/v1/admin/domain_name'
      

      如果请求成功,你会得到以下输出:

      {"status":true, "domainName":"haomingsu.cn"}
      
    4. 检查域名是否添加成功, 注意在 url (/v1/{username}/domain_name) 中的 {username} 需要改成你自己的用户名 (例如 admin), token 在前两步中获得

      cURL:

      curl --location --request GET 'http://127.0.0.1:50001/v1/admin/domain_name' \
      --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ'
      

      wget:

      wget --no-check-certificate --quiet \
        --method GET \
        --timeout=0 \
        --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ' \
         'http://127.0.0.1:50001/v1/admin/domain_name'
      

      如果请求成功,你会得到以下输出:

      {"domainNames":["haomingsu.cn"]}
      

容器镜像仓库

  • Docker Hub:

    docker pull hominsu/ali-ddns-client-service:latest
    docker pull hominsu/ali-ddns-server-service:latest
    
  • GitHub Container Repository:

    docker pull ghcr.io/hominsu/ali-ddns-client-service:latest
    docker pull ghcr.io/hominsu/ali-ddns-server-service:latest
    
  • Ali Container Repository: registry.cn-shenzhen.aliyuncs.com/hominsu/ali-ddns-client:latest

    docker pull registry.cn-shenzhen.aliyuncs.com/hominsu/ali-ddns-client-service:latest
    docker pull registry.cn-shenzhen.aliyuncs.com/hominsu/ali-ddns-server-service:latest
    

其他

在服务端,日志会保存在 ads/logs/ads.log

[root@iZwz9diii276grug5qq3byZ ddns-server-service]# cat ads/logs/ads.log 
2022-01-26T13:59:54.423+0800    info    runtime/proc.go:255     service.id: e4fd4c9652af, service.name: ali-ddns-server-service, service.version: 1.2.5, git sha1: a3936d5d8b6044bbbed686d6b2222c2c5813fa39, build stamp: 1643173896
2022-01-26T13:59:54.428+0800    info    grpclog/grpclog.go:37   [core]original dial target is: "localhost:50003"        {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]parsed dial target is: {Scheme:localhost Authority: Endpoint:50003 URL:{Scheme:localhost Opaque:50003 User: Host: Path: RawPath: ForceQuery:false RawQuery: Fragment: RawFragment:}}    {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]fallback to scheme "passthrough"  {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]parsed dial target is: {Scheme:passthrough Authority: Endpoint:localhost:50003 URL:{Scheme:passthrough Opaque: User: Host: Path:/localhost:50003 RawPath: ForceQuery:false RawQuery: Fragment: RawFragment:}}   {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]Channel authority set to "localhost"      {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]ccResolverWrapper: sending update to cc: {[{localhost:50003  <nil> <nil> 0 <nil>}] <nil> <nil>} {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]ClientConn switching balancer to "pick_first"     {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]Channel switches to new LB policy "pick_first"    {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]Subchannel Connectivity change to CONNECTING      {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]Subchannel picks a new address "localhost:50003" to connect       {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]pickfirstBalancer: UpdateSubConnState: 0xc000171e40, {CONNECTING <nil>}   {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.429+0800    info    grpclog/grpclog.go:37   [core]Channel Connectivity change to CONNECTING {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.444+0800    info    grpclog/grpclog.go:37   [core]Subchannel Connectivity change to READY   {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.444+0800    info    grpclog/grpclog.go:37   [core]pickfirstBalancer: UpdateSubConnState: 0xc000171e40, {READY <nil>}        {"system": "grpc", "grpc_log": true}
2022-01-26T13:59:54.444+0800    info    grpclog/grpclog.go:37   [core]Channel Connectivity change to READY      {"system": "grpc", "grpc_log": true}
2022-01-26T14:13:57.084+0800    info    zap/server_interceptors.go:39   finished unary call with code OK        {"grpc.start_time": "2022-01-26T14:13:57+08:00", "system": "grpc", "span.kind": "server", "grpc.service": "server.service.v1.DomainService", "grpc.method": "GetDomainRecord", "grpc.code": "OK", "grpc.time_ms": 1.3580000400543213}
2022-01-26T14:23:57.094+0800    info    zap/server_interceptors.go:39   finished unary call with code OK        {"grpc.start_time": "2022-01-26T14:23:57+08:00", "system": "grpc", "span.kind": "server", "grpc.service": "server.service.v1.DomainService", "grpc.method": "GetDomainRecord", "grpc.code": "OK", "grpc.time_ms": 1.2719999551773071}
2022-01-26T14:33:44.731+0800    warn    grpclog/grpclog.go:46   [core]grpc: Server.Serve failed to create ServerTransport: connection error: desc = "ServerHandshake(\"47.103.37.203:36666\") failed: tls: client didn't provide a certificate"       {"system": "grpc", "grpc_log": true}
2022-01-26T14:33:44.797+0800    warn    grpclog/grpclog.go:46   [core]grpc: Server.Serve failed to create ServerTransport: connection error: desc = "ServerHandshake(\"47.103.37.203:36682\") failed: tls: first record does not look like a TLS handshake"   {"system": "grpc", "grpc_log": true}
2022-01-26T14:33:44.897+0800    warn    grpclog/grpclog.go:46   [core]grpc: Server.Serve failed to create ServerTransport: connection error: desc = "ServerHandshake(\"47.103.37.203:36696\") failed: tls: client didn't provide a certificate"       {"system": "grpc", "grpc_log": true}
2022-01-26T14:33:44.954+0800    warn    grpclog/grpclog.go:46   [core]grpc: Server.Serve failed to create ServerTransport: connection error: desc = "ServerHandshake(\"47.103.37.203:36710\") failed: tls: first record does not look like a TLS handshake"   {"system": "grpc", "grpc_log": true}
2022-01-26T14:33:57.008+0800    info    zap/server_interceptors.go:39   finished unary call with code OK        {"grpc.start_time": "2022-01-26T14:33:57+08:00", "system": "grpc", "span.kind": "server", "grpc.service": "server.service.v1.DomainService", "grpc.method": "GetDomainRecord", "grpc.code": "OK", "grpc.time_ms": 1.125}