Ali-DDNS
DDNS service by using Ali openapi
Description
DDNS service by using Ali openapi
Details
# 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 | +------------+
+---+ +---+ +---+
How to use
Clone the project both server and client:
git clone --depth=1 https://github.com/hominsu/Ali-DDNS.git
The connections between the
client-side
and theserver-side
is authenticated viatls
, so the relevant certificate must be created first, here using openssl to generateSANs
certsCreate CA's certs:
❯ 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
Prepare the openssl configuration file
copy the default openssl to current dir
linux
:cp /etc/pki/tls/openssl.cnf .
macos
:cp /System/Library/OpenSSL/openssl.cnf .
Modify the following options in
openssl.cnf
Find
[ CA_default ]
and uncommentcopy_extensions = copy
:[ CA_default ] # Extension copying option: use with caution. copy_extensions = copy
Find
[ req ]
and uncommentreq_extensions = v3_req
:[ req ] req_extensions = v3_req # The extensions to add to a certificate request
Find
[ v3_req ]
and addsubjectAltName = @alt_names
, then add the new tag[ alt_names ]
and the field:[ 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
Generate server-side certs
❯ 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
Generate client-side certs
❯ 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
The full file is shown below:
❯ 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
In the server-side, the
ca.crt
,server.pem
,server.key
is using to set credentials for server connections, and thegrpc-gateway
will use theca.crt
,client.pem
,client.key
to set credentials for connections betweengrpc-gateway
andgrpc server
In the client-side, for safety reasons, you should generate the certs for each client with the CA's certs (
ca.crt
andca.key
) if others use your grpc-services, or you can just use the same certs as gateway (not recommended)Then create a cert directory and copy the certs into it
On the server side, the file structure is shown below:
❯ tree cert cert ├── ca.crt ├── client.key ├── client.pem ├── server.key └── server.pem 0 directories, 5 files
On the client side, the file structure is shown below:
❯ tree cert cert ├── ca.crt ├── client.key └── client.pem 0 directories, 3 files
In server-service, fill in your Ali Access-Key in the
docker-compose.yml
, set your redis db password. The interface use jwt for authentication, may sure you set the jwt token (e.g.homing.so
). Usually the operator updates the public IP in the early morning, so use the Cron expression (e.g.CRON_TZ=Asia/Shanghai 1/10 2-4 * * *
) to specify that it updates at a higher frequency during the early morning hours and at a slower rate (like once per hour) at other times. Of course you can define your own time.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 执行一次
Up the server-service
cd Ali-DDNS/deploy/docker-compose/ddns-server docker-compose up -d
Up the client-service
cd Ali-DDNS/deploy/docker-compose/ddns-client docker-compose up -d
Configuring the server service
Register a user
Send an http request via
cURL
:curl --location --request POST 'http://127.0.0.1:50001/v1/register' \ --header 'Content-Type: application/json' \ --data-raw '{ "username": "admin", "password": "passwd" }'
Or use
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'
You will get the following output if the request is successful
{"status":true}
Login to get the 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'
You will get the following output with
token
andusername
if the request is successful{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VybmFtZSI6ImFkbWluIiwiaXNzIjoiMTI3LjAuMC4xIiwic3ViIjoidXNlciB0b2tlbiIsImV4cCI6MTY0MzEwNTY3MSwiaWF0IjoxNjQzMTAyMDcxfQ.EmYB_PApYocKSbdyT0ykUMPMJErMykv3AASBcYngJTQ", "username":"admin"}
Add the domain name you need to monitor, note that the
{username}
in the url (/v1/{username}/domain_name
) needs to be your own username (e.g.admin
), and the token is obtained in the previous stepcURL
: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'
You will get the following output if the request is successful
{"status":true, "domainName":"haomingsu.cn"}
Check the domain name is added, note that the
{username}
in the url (/v1/{username}/domain_name
) needs to be your own username (e.g.admin
), and the token is obtained in the first two stepscURL
: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'
You will get the following output if the request is successful
{"domainNames":["haomingsu.cn"]}
Container Repository
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
Other
On the server-side, the logs are stored in the 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}