mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-26 02:52:34 -04:00
Compare commits
323 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6f72d97bc | ||
|
|
5fff4f4a56 | ||
|
|
41d848f9a7 | ||
|
|
e42feb933f | ||
|
|
e7ebfc4b9a | ||
|
|
23043b73d9 | ||
|
|
4fb7526ce7 | ||
|
|
43d405cfd8 | ||
|
|
b0493e9c1c | ||
|
|
28509cfb36 | ||
|
|
522cc7c9c6 | ||
|
|
519b9c4380 | ||
|
|
464a557adb | ||
|
|
5ae4668296 | ||
|
|
934c67d710 | ||
|
|
9e6d456fc9 | ||
|
|
15c6ad4b01 | ||
|
|
3339c838fa | ||
|
|
18751973c1 | ||
|
|
49b779813a | ||
|
|
f779a2159e | ||
|
|
43f47fdee0 | ||
|
|
0e3703b8f2 | ||
|
|
4a2a6d83d5 | ||
|
|
1aebdef825 | ||
|
|
8819e5f030 | ||
|
|
228fc507a7 | ||
|
|
eaa7200734 | ||
|
|
bfbd68f9d5 | ||
|
|
83a9a2d999 | ||
|
|
8a18aec420 | ||
|
|
3fd82ab89f | ||
|
|
2165b69137 | ||
|
|
3ad972b718 | ||
|
|
e197478638 | ||
|
|
3b0fae84e5 | ||
|
|
46b1d649f0 | ||
|
|
bd4b87032f | ||
|
|
59bffe99ba | ||
|
|
d8de9d25ad | ||
|
|
ba8cd00326 | ||
|
|
9671407502 | ||
|
|
3a3c892385 | ||
|
|
cd37ab1646 | ||
|
|
7c815924ff | ||
|
|
675c4ae64e | ||
|
|
71b9b92288 | ||
|
|
23ec8d7907 | ||
|
|
89711cd76d | ||
|
|
f09ca2dd0a | ||
|
|
251112697a | ||
|
|
7fcd0f902b | ||
|
|
88474c8a3a | ||
|
|
5674102019 | ||
|
|
41a64bbd55 | ||
|
|
f793454476 | ||
|
|
bcbf0938c1 | ||
|
|
1561babd76 | ||
|
|
45c684232d | ||
|
|
d725c7c856 | ||
|
|
bc081ef3c1 | ||
|
|
a688255270 | ||
|
|
0655f1a080 | ||
|
|
b3a7669d2a | ||
|
|
f455c2a880 | ||
|
|
584597d5cb | ||
|
|
1f9eb467ca | ||
|
|
b1a69e028e | ||
|
|
39c3732202 | ||
|
|
b2634a15fa | ||
|
|
5b426afc57 | ||
|
|
f5b76f214e | ||
|
|
e4f0c0d561 | ||
|
|
0f28807240 | ||
|
|
d1fbbbe9d4 | ||
|
|
ae7f10448d | ||
|
|
01334ad91f | ||
|
|
fa15abb781 | ||
|
|
87ffce17b1 | ||
|
|
7382c5bb92 | ||
|
|
5b657dfa92 | ||
|
|
ecda99a217 | ||
|
|
7fccf2c979 | ||
|
|
14cd7b0a47 | ||
|
|
d0f03c8ae1 | ||
|
|
7acc9b931a | ||
|
|
ea215dec6b | ||
|
|
87f06eeb28 | ||
|
|
963da5aab7 | ||
|
|
9e73f99ee1 | ||
|
|
01f4044404 | ||
|
|
904c59d6ac | ||
|
|
242c47bbed | ||
|
|
00a1d25847 | ||
|
|
8b327f81c7 | ||
|
|
b29d6bb9d2 | ||
|
|
cdbebc2323 | ||
|
|
335c82b4be | ||
|
|
28e2d00198 | ||
|
|
c584f9b3d5 | ||
|
|
51dd14cb66 | ||
|
|
41e61ddaa6 | ||
|
|
ee69feaaea | ||
|
|
9252bca505 | ||
|
|
8f8d187a32 | ||
|
|
d837acd3a2 | ||
|
|
ad57321ea3 | ||
|
|
e20a9b8634 | ||
|
|
5847e9fe8f | ||
|
|
e608a016db | ||
|
|
050b34a9bd | ||
|
|
96cf980b36 | ||
|
|
3fc3b115f8 | ||
|
|
6a3789f78c | ||
|
|
cd6d46e146 | ||
|
|
6fdbaa1b97 | ||
|
|
4684dfc9a3 | ||
|
|
9b094fff7a | ||
|
|
a4af81dddb | ||
|
|
e735ef335d | ||
|
|
3ece0c967e | ||
|
|
523bbee5b8 | ||
|
|
86f559c0b3 | ||
|
|
4be365574d | ||
|
|
10a952f7b9 | ||
|
|
3606f7a461 | ||
|
|
ae24ab8d56 | ||
|
|
bc1a5b206f | ||
|
|
2dceab7452 | ||
|
|
1e74dc6179 | ||
|
|
fe00994522 | ||
|
|
d22dc41903 | ||
|
|
76b1f5f0df | ||
|
|
c5de1358dd | ||
|
|
4133dbfdc7 | ||
|
|
c4f50992c2 | ||
|
|
f8b67e5261 | ||
|
|
1b154df8aa | ||
|
|
f9edf75540 | ||
|
|
ae748fb80d | ||
|
|
edb6c491bf | ||
|
|
2f196b4886 | ||
|
|
2d7d016650 | ||
|
|
1da19b3d6d | ||
|
|
0c520eddda | ||
|
|
050825d399 | ||
|
|
53212df518 | ||
|
|
6fe4743bc0 | ||
|
|
4cdc71b45c | ||
|
|
36a6275bd2 | ||
|
|
ee66170a87 | ||
|
|
bb62a9d9e1 | ||
|
|
e04c1f83df | ||
|
|
e19cabc894 | ||
|
|
682821efd0 | ||
|
|
276c1c02fd | ||
|
|
9b8ec3298b | ||
|
|
cc670b3939 | ||
|
|
0c3d258620 | ||
|
|
d39ecd6fd3 | ||
|
|
43dd02dbd5 | ||
|
|
381af0cd90 | ||
|
|
2bede070de | ||
|
|
09b810732f | ||
|
|
398566850d | ||
|
|
201df818d3 | ||
|
|
b1a3740059 | ||
|
|
ae978e007b | ||
|
|
3688208030 | ||
|
|
da4341fbba | ||
|
|
1705afeada | ||
|
|
844904aa7b | ||
|
|
803232e670 | ||
|
|
2f7ee832b8 | ||
|
|
943d1486b3 | ||
|
|
d727feefc4 | ||
|
|
aee3022a11 | ||
|
|
864a696680 | ||
|
|
34623e1b81 | ||
|
|
c0e57c6e1d | ||
|
|
1ff629dc17 | ||
|
|
23f4897365 | ||
|
|
e3472476e2 | ||
|
|
869cf52c7d | ||
|
|
8b2e83a69e | ||
|
|
106bcae966 | ||
|
|
0a89666d1d | ||
|
|
3ad9168576 | ||
|
|
1eaa495e66 | ||
|
|
5e0cc687ea | ||
|
|
289536b145 | ||
|
|
d5e4936679 | ||
|
|
6e18e921b7 | ||
|
|
40c92eebf5 | ||
|
|
8adc0556ba | ||
|
|
e7824014ee | ||
|
|
6e0d48f7c1 | ||
|
|
a10fb64ffd | ||
|
|
a649f8a4a8 | ||
|
|
72018451b3 | ||
|
|
2e5b3c7924 | ||
|
|
c985595969 | ||
|
|
caa4840a61 | ||
|
|
a86ddd42d1 | ||
|
|
ce1a447e0a | ||
|
|
22c8b587c9 | ||
|
|
78c86b0e24 | ||
|
|
fa771cd4b2 | ||
|
|
4e94823f69 | ||
|
|
96521addd5 | ||
|
|
85ddad6da1 | ||
|
|
d54c334e1c | ||
|
|
49392003e0 | ||
|
|
5a5e0c7375 | ||
|
|
180092f47e | ||
|
|
43e168581c | ||
|
|
76a40e47c7 | ||
|
|
50b1997dbb | ||
|
|
d75398d71b | ||
|
|
7191e93932 | ||
|
|
ec1f7acbde | ||
|
|
a8a8d5793b | ||
|
|
dde28ebd1f | ||
|
|
cd5f04acaf | ||
|
|
5d1809aca6 | ||
|
|
46ae9af6a4 | ||
|
|
b9a357fc1c | ||
|
|
1e7c909baf | ||
|
|
e83857b3db | ||
|
|
5ab1607f97 | ||
|
|
4bb09e86dd | ||
|
|
9460fa969b | ||
|
|
157e041b28 | ||
|
|
b40391bb4e | ||
|
|
60cf3130e3 | ||
|
|
5557c32135 | ||
|
|
46e8647d35 | ||
|
|
eb1f925576 | ||
|
|
18b058f188 | ||
|
|
be89e798d2 | ||
|
|
20704eea8c | ||
|
|
6416c6fed3 | ||
|
|
a317809ff8 | ||
|
|
7ae9f72038 | ||
|
|
5b1bc29ad4 | ||
|
|
e937ee741c | ||
|
|
688aebbc4f | ||
|
|
6c2c2cf7cb | ||
|
|
1c3e4cdef7 | ||
|
|
d8a7fef6bd | ||
|
|
87f59bf498 | ||
|
|
818f7471dd | ||
|
|
b35658f32c | ||
|
|
48fb1a4b95 | ||
|
|
2e069ccf1d | ||
|
|
0d38e9385f | ||
|
|
b4310ad822 | ||
|
|
878cf4cc45 | ||
|
|
89faafe377 | ||
|
|
bf3f78f9f9 | ||
|
|
6bf82d7be1 | ||
|
|
77f1681c4c | ||
|
|
82e3d60db5 | ||
|
|
c65c8d63bd | ||
|
|
0f1ddfb9b0 | ||
|
|
1d466cdc83 | ||
|
|
0a6ef7ffbf | ||
|
|
e7ae5ac4cc | ||
|
|
701ae125bf | ||
|
|
b775798b65 | ||
|
|
166b0c7095 | ||
|
|
3240bb7d26 | ||
|
|
3047c83ee9 | ||
|
|
3b29a33849 | ||
|
|
860bc02e2e | ||
|
|
cd2d3c46cc | ||
|
|
e630dd9889 | ||
|
|
b2d96787b8 | ||
|
|
5b52255601 | ||
|
|
04c55028a1 | ||
|
|
e07e8da31c | ||
|
|
af2f3a3257 | ||
|
|
00efca4af4 | ||
|
|
3272612db2 | ||
|
|
5b7968c468 | ||
|
|
7ec39c1256 | ||
|
|
96857f3bae | ||
|
|
57c7d1be2d | ||
|
|
53b6f533a8 | ||
|
|
a82ed93169 | ||
|
|
d07fcc664b | ||
|
|
d013ceb869 | ||
|
|
d3506e9792 | ||
|
|
c0249672bf | ||
|
|
cfeb95872a | ||
|
|
091cf5f855 | ||
|
|
7001add533 | ||
|
|
9ccdbbd2d3 | ||
|
|
f8a3ab63c6 | ||
|
|
14033223d9 | ||
|
|
18161a6064 | ||
|
|
216b8ab1ae | ||
|
|
4af23a756b | ||
|
|
96c84a5a4f | ||
|
|
4564d16c06 | ||
|
|
6e4a56b3cf | ||
|
|
919ab65985 | ||
|
|
e023aae909 | ||
|
|
066bcdd3bf | ||
|
|
0a6c6b9bc7 | ||
|
|
8cdff8e4d3 | ||
|
|
bffc30264f | ||
|
|
4f141c20a0 | ||
|
|
582ce626c8 | ||
|
|
13b29aeba4 | ||
|
|
a5afca15d1 | ||
|
|
25ee692242 | ||
|
|
922146efd3 | ||
|
|
db9959cf59 | ||
|
|
50303aef7b | ||
|
|
137e372102 | ||
|
|
f300c59411 | ||
|
|
c23998bd09 |
5
.devcontainer/.dockerignore
Normal file
5
.devcontainer/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1 @@
|
||||
FROM qmcgaw/godevcontainer
|
||||
69
.devcontainer/README.md
Normal file
69
.devcontainer/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Development container
|
||||
|
||||
Development container that can be used with VSCode.
|
||||
|
||||
It works on Linux, Windows and OSX.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the following files on your host if you don't have them:
|
||||
|
||||
```sh
|
||||
touch ~/.gitconfig ~/.zsh_history
|
||||
```
|
||||
|
||||
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
|
||||
|
||||
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
### Customize the image
|
||||
|
||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||
|
||||
```Dockerfile
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add curl
|
||||
```
|
||||
|
||||
To rebuild the image, either:
|
||||
|
||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
||||
- With a terminal, go to this directory and `docker-compose build`
|
||||
|
||||
### Customize VS code settings
|
||||
|
||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
|
||||
|
||||
### Publish a port
|
||||
|
||||
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml). You can also now do it directly with VSCode without restarting the container.
|
||||
|
||||
### Run other services
|
||||
|
||||
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
|
||||
|
||||
```yml
|
||||
database:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password
|
||||
```
|
||||
|
||||
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
|
||||
1. In the VS code command palette, rebuild the container.
|
||||
@@ -1,117 +1,75 @@
|
||||
{
|
||||
"name": "ddns-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "go mod download",
|
||||
"workspaceFolder": "/workspace",
|
||||
"appPort": 8000,
|
||||
"extensions": [
|
||||
"ms-vscode.go",
|
||||
"IBM.output-colorizer",
|
||||
"eamodio.gitlens",
|
||||
"mhutchie.git-graph",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"shardulm94.trailing-spaces",
|
||||
"alefragnani.Bookmarks",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"mohsen1.prettify-json",
|
||||
"quicktype.quicktype",
|
||||
"spikespaz.vscode-smoothtype",
|
||||
"stkb.rewrap",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
],
|
||||
"settings": {
|
||||
// General settings
|
||||
"files.eol": "\n",
|
||||
// Docker
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
// Golang general settings
|
||||
"go.useLanguageServer": true,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"gopls": {
|
||||
"completeUnimported": true,
|
||||
"deepCompletion": true,
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
// Golang testing
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags=integration"
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags=integration"
|
||||
},
|
||||
"go.testEnvVars": {},
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
}
|
||||
{
|
||||
"name": "ddns-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
||||
"workspaceFolder": "/workspace",
|
||||
// "overrideCommand": "",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"eamodio.gitlens", // IDE Git information
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||
"bierner.emojisense", // Emoji sense for markdown
|
||||
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||
"github.vscode-pull-request-github", // Github interaction
|
||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||
"IBM.output-colorizer", // Colorize your output/test logs
|
||||
"github.copilot" // AI code completion
|
||||
// "mohsen1.prettify-json", // Prettify JSON data
|
||||
// "zxh404.vscode-proto3", // Supports Proto syntax
|
||||
// "jrebocho.vscode-random", // Generates random values
|
||||
// "alefragnani.Bookmarks", // Manage bookmarks
|
||||
// "quicktype.quicktype", // Paste JSON as code
|
||||
// "spikespaz.vscode-smoothtype", // smooth cursor animation
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"go.buildTags": "",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"go.testEnvVars": {
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"go.testFlags": [
|
||||
"-v",
|
||||
"-race"
|
||||
],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnTestPackage": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"vulncheck": "Imports"
|
||||
},
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,26 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
image: qmcgaw/godevcontainer
|
||||
build: .
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
- ~/.ssh:/home/vscode/.ssh:ro
|
||||
- ~/.ssh:/root/.ssh:ro
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
# created in the container. On Windows, files are copied
|
||||
# from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
- ~/.ssh:/mnt/ssh
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
environment:
|
||||
- TZ=
|
||||
cap_add:
|
||||
# For debugging with dlv
|
||||
- SYS_PTRACE
|
||||
security_opt:
|
||||
# For debugging with dlv
|
||||
- seccomp:unconfined
|
||||
entrypoint: zsh -c "while sleep 1000; do :; done"
|
||||
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
docs
|
||||
readme
|
||||
.gitignore
|
||||
config.json
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
ui/favicon.svg
|
||||
|
||||
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Report a bug
|
||||
title: 'Bug: ...'
|
||||
labels: ":bug: bug"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature to add to this project
|
||||
title: 'Feature request: ...'
|
||||
labels: ":bulb: feature request"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
1. What's the feature?
|
||||
|
||||
2. Extra information?
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Help
|
||||
about: Ask for help
|
||||
title: 'Help: ...'
|
||||
labels: ":pray: help wanted"
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
HAVE A CHAT FIRST!
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
110
.github/workflows/build.yml
vendored
110
.github/workflows/build.yml
vendored
@@ -1,12 +1,108 @@
|
||||
name: Docker build
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
jobs:
|
||||
build:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
# We run this here to use the caching of the previous steps
|
||||
- name: Build final image
|
||||
run: docker build .
|
||||
|
||||
publish:
|
||||
needs: [verify]
|
||||
if: |
|
||||
github.repository == 'qdm12/ddns-updater' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: docker build .
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set variables
|
||||
id: vars
|
||||
run: |
|
||||
BRANCH=${GITHUB_REF#refs/heads/}
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo ::set-output name=commit::$(git rev-parse --short HEAD)
|
||||
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
if [ "$TAG" != "$GITHUB_REF" ]; then
|
||||
echo ::set-output name=version::$TAG
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
elif [ "$BRANCH" = "master" ]; then
|
||||
echo ::set-output name=version::latest
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
else
|
||||
echo ::set-output name=version::${BRANCH##*/}
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
fi
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: ${{ steps.vars.outputs.platforms }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||
COMMIT=${{ steps.vars.outputs.commit }}
|
||||
VERSION=${{ steps.vars.outputs.version }}
|
||||
tags: |
|
||||
qmcgaw/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
ghcr.io/${{ github.repository_owner }}/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
push: true
|
||||
|
||||
40
.github/workflows/buildx-latest.yml
vendored
40
.github/workflows/buildx-latest.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx latest
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=latest \
|
||||
-t qmcgaw/ddns-updater:latest \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
40
.github/workflows/buildx-release.yml
vendored
40
.github/workflows/buildx-release.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=${GITHUB_REF##*/} \
|
||||
-t qmcgaw/ddns-updater:${GITHUB_REF##*/} \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
4
.github/workflows/dockerhub-description.yml
vendored
4
.github/workflows/dockerhub-description.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2.1.0
|
||||
uses: peter-evans/dockerhub-description@v3.1.0
|
||||
env:
|
||||
DOCKERHUB_USERNAME: qmcgaw
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
11
.github/workflows/greetings.yml
vendored
11
.github/workflows/greetings.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Greetings
|
||||
on: [pull_request, issues]
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thanks for creating your first issue :+1: Feel free to use [Slack](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc) if you just need some quick help or want to chat'
|
||||
pr-message: 'Thank you so much for contributing, that means a lot to me :wink:'
|
||||
14
.github/workflows/labels.yml
vendored
14
.github/workflows/labels.yml
vendored
@@ -1,18 +1,18 @@
|
||||
name: labels
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/labels.yml'
|
||||
- '.github/workflows/labels.yml'
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Labeler
|
||||
if: success()
|
||||
uses: crazy-max/ghaction-github-labeler@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: crazy-max/ghaction-github-labeler@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
16
.github/workflows/misspell.yml
vendored
16
.github/workflows/misspell.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Misspells
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
misspell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: reviewdog/action-misspell@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
locale: "US"
|
||||
level: error
|
||||
59
.github/workflows/security.yml
vendored
59
.github/workflows/security.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Security scan of Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
schedule:
|
||||
- cron: '0 9 * * *'
|
||||
jobs:
|
||||
security-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Check for scratch
|
||||
id: scratchCheck
|
||||
run: echo ::set-output name=scratch::$(cat Dockerfile | grep 'FROM scratch')
|
||||
- name: Build image
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
run: docker build -t image .
|
||||
- name: Phonito
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: phonito/phonito-scanner-action@master
|
||||
with:
|
||||
image: image
|
||||
fail-level: LOW
|
||||
phonito-token: ${{ secrets.PHONITO_TOKEN }}
|
||||
- name: Trivy
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: homoluctus/gitrivy@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
image: image
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
data
|
||||
@@ -4,46 +4,84 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- goerr113
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# - cyclop
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- containedctx
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- dupword
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- execinquery
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- grouper
|
||||
- importas
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- misspell
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- sqlclosecheck
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.26.x # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
88
.vscode/settings.json
vendored
88
.vscode/settings.json
vendored
@@ -1,88 +0,0 @@
|
||||
{
|
||||
// General settings
|
||||
"files.eol": "\n",
|
||||
// Docker
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
// Golang general settings
|
||||
"go.useLanguageServer": true,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"gopls": {
|
||||
"completeUnimported": true,
|
||||
"deepCompletion": true,
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
// Golang testing
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"go.testEnvVars": {},
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
}
|
||||
117
Dockerfile
117
Dockerfile
@@ -1,54 +1,93 @@
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.14
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.20
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.52.2
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
ARG GOLANGCI_LINT_VERSION=v1.26.0
|
||||
RUN apk --update add git
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
ENV CGO_ENABLED=0
|
||||
RUN apk --update add git g++
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download 2>&1
|
||||
RUN go mod download
|
||||
COPY pkg/ ./pkg/
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN go test ./...
|
||||
RUN go build -ldflags="-s -w" -o app
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS test
|
||||
# Note on the go race detector:
|
||||
# - we set CGO_ENABLED=1 to have it enabled
|
||||
# - we installed g++ to support the race detector
|
||||
ENV CGO_ENABLED=1
|
||||
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS build
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
ARG TARGETPLATFORM
|
||||
RUN GOARCH="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arch)" \
|
||||
GOARM="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arm)" \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
-X 'main.version=$VERSION' \
|
||||
-X 'main.buildDate=$BUILD_DATE' \
|
||||
-X 'main.commit=$COMMIT' \
|
||||
" -o app cmd/updater/main.go
|
||||
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
USER ${UID}:${GID}
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
UPDATE_COOLDOWN_PERIOD=5m \
|
||||
PUBLICIP_FETCHERS=all \
|
||||
PUBLICIP_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV4_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV6_HTTP_PROVIDERS=all \
|
||||
PUBLICIP_DNS_PROVIDERS=all \
|
||||
PUBLICIP_DNS_TIMEOUT=3s \
|
||||
HTTP_TIMEOUT=10s \
|
||||
DATADIR=/updater/data \
|
||||
RESOLVER_ADDRESS= \
|
||||
RESOLVER_TIMEOUT=5s \
|
||||
# Web UI
|
||||
LISTENING_PORT=8000 \
|
||||
ROOT_URL=/ \
|
||||
# Backup
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
# Other
|
||||
LOG_LEVEL=info \
|
||||
LOG_CALLER=hidden \
|
||||
SHOUTRRR_ADDRESSES= \
|
||||
TZ=
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$COMMIT \
|
||||
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Namecheap, Cloudflare, GoDaddy, DuckDns, Dreamhost, DNSPod and NoIP"
|
||||
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
USER 1000
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV DELAY=10m \
|
||||
ROOT_URL=/ \
|
||||
LISTENING_PORT=8000 \
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=0 \
|
||||
HTTP_TIMEOUT=10s \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN= \
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI"
|
||||
COPY --from=build --chown=${UID}:${GID} /tmp/gobuild/app /updater/app
|
||||
|
||||
736
README.md
736
README.md
@@ -1,390 +1,346 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
*Light container updating DNS A records periodically for GoDaddy, Namecheap, Cloudflare, Dreamhost, NoIP, DNSPod, Infomaniak, ddnss.de and DuckDNS*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers: Namecheap, GoDaddy, Cloudflare, NoIP, Dreamhost, DuckDNS, DNSPod and Infomaniak (ask for more)
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 12.3MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
|
||||
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
1. To setup your domains initially, see the [Domain set up](#domain-set-up) section.
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
mkdir data
|
||||
touch data/config.json
|
||||
# Owned by user ID of Docker container (1000)
|
||||
chown -R 1000 data
|
||||
# all access (for creating json database file data/updates.json)
|
||||
chmod 700 data
|
||||
# read access only
|
||||
chmod 400 data/config.json
|
||||
```
|
||||
|
||||
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
|
||||
|
||||
1. Modify the *data/config.json* file similarly to:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "duckduckgo",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See more information in the [configuration section](#configuration)
|
||||
|
||||
1. Use the following command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
1. You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added in *config.json*
|
||||
|
||||
For all record update configuration, you need the following:
|
||||
|
||||
- `"provider"` is the DNS provider and can be `"godaddy"`, `"namecheap"`, `"duckdns"`, `"dreamhost"`, `"cloudflare"`, `"noip"`, `"dnspod"` or `"ddnss"`
|
||||
- `"domain"`
|
||||
- `"ip_method"` is the method to obtain your public IP address and can be:
|
||||
- `"provider"` means the public IP is automatically determined by the DNS provider (**only for DuckDNs, Namecheap, Infomaniak and NoIP**), most reliable.
|
||||
- `"opendns"` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip) (reliable)
|
||||
- `"ifconfig"` using [https://ifconfig.io/ip](https://ifconfig.io/ip) (may be rate limited)
|
||||
- `"ipinfo"` using [https://ipinfo.io/ip](https://ipinfo.io/ip) (may be rate limited)
|
||||
- `"ipify"` using [https://api.ipify.org](https://api.ipify.org) (may be rate limited)
|
||||
- `"ipify6"` using [https://api6.ipify.org](https://api.ipify.org) for IPv6 only (may be rate limited)
|
||||
- `"ddnss"` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php) for IPv4 only
|
||||
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php) for IPv6 only
|
||||
- `"cycle"` to cycle between each external methods, in order to avoid being rate limited
|
||||
- You can also specify an HTTPS URL to obtain your public IP address (i.e. `"ip_method": "https://ipinfo.io/ip"`)
|
||||
|
||||
You can optionnally add the parameters:
|
||||
|
||||
- `"delay"` is the delay in seconds between each update. It defaults to the `DELAY` environment variable value.
|
||||
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the periodic Docker healthcheck from running a DNS lookup on your domain.
|
||||
|
||||
For each DNS provider exist some specific parameters you need to add, as described below:
|
||||
|
||||
Namecheap:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
Cloudflare:
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"identifier"` is the DNS record identifier as returned by the Cloudflare "List DNS Records" API (see below)
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
|
||||
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
|
||||
|
||||
GoDaddy:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
DuckDNS:
|
||||
|
||||
- `"token"`
|
||||
|
||||
Dreamhost:
|
||||
|
||||
- `"key"`
|
||||
|
||||
NoIP:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
DNSPOD:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
Infomaniak:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
|
||||
DDNSS.de:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `DELAY` | `10m` | Default delay between updates, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `LOG_ENCODING` | `console` | Format of logging, `json` or `console` |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
|
||||
| `NODE_ID` | `0` | Node ID (for distributed systems), can be any integer |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
|
||||
|
||||
### Host firewall
|
||||
|
||||
If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
- TCP 443 outbound for outbound HTTPS
|
||||
- TCP 80 outbound if you use a local unsecured HTTP connection to your Gotify server
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Domain set up
|
||||
|
||||
### Namecheap
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
|
||||

|
||||
|
||||
1. Select the following settings and create the *A + Dynamic DNS Record*:
|
||||
|
||||

|
||||
|
||||
1. Scroll down and turn on the switch for *DYNAMIC DNS*
|
||||
|
||||

|
||||
|
||||
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
### GoDaddy
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
***
|
||||
|
||||
### DuckDNS
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See [duckdns website](https://duckdns.org)*
|
||||
|
||||
### Cloudflare
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
|
||||
## Gotify
|
||||
|
||||
[](https://gotify.net)
|
||||
|
||||
[**Gotify**](https://gotify.net) is a simple server for sending and receiving messages, and it is **free**, **private** and **open source**
|
||||
|
||||
- It has an [Android app](https://play.google.com/store/apps/details?id=com.github.gotify) to receive notifications
|
||||
- The app does not drain your battery 👍
|
||||
- The notification server is self hosted, see [how to set it up with Docker](https://gotify.net/docs/install)
|
||||
- The notifications only go through your own server (ideally through HTTPS though)
|
||||
|
||||
To set it up with DDNS updater:
|
||||
|
||||
1. Go to the Web GUI of Gotify
|
||||
1. Login with the admin credentials
|
||||
1. Create an app and copy the generated token to the environment variable `GOTIFYTOKEN` (for this container)
|
||||
1. Set the `GOTIFYURL` variable to the URL of your Gotify server address (i.e. `http://127.0.0.1:8080` or `https://bla.com/gotify`)
|
||||
|
||||
## Testing
|
||||
|
||||
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
|
||||
- You can check manually at:
|
||||
- GoDaddy: [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
|
||||
You might want to try to change the IP address to `127.0.0.1` to see if the update actually occurs.
|
||||
|
||||
## Development
|
||||
|
||||
1. Setup your environment
|
||||
|
||||
<details><summary>Using VSCode and Docker (easier)</summary><p>
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1: So you can discard it and update it easily!
|
||||
|
||||
</p></details>
|
||||
|
||||
<details><summary>Locally</summary><p>
|
||||
|
||||
1. Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads)
|
||||
1. Install Go dependencies with
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install)
|
||||
1. You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](https://github.com/qdm12/ddns-updater/master/.vscode/settings.json).
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Commands available:
|
||||
|
||||
```sh
|
||||
# Build the binary
|
||||
go build cmd/app/main.go
|
||||
# Test the code
|
||||
go test ./...
|
||||
# Lint the code
|
||||
golangci-lint run
|
||||
# Build the Docker image
|
||||
docker build -t qmcgaw/ddns-updater .
|
||||
```
|
||||
|
||||
1. See [Contributing](https://github.com/qdm12/ddns-updater/master/.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
|
||||
## Used in external projects
|
||||
|
||||
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Update dependencies
|
||||
- [ ] Mockgen instead of mockery
|
||||
- [ ] Other types or records
|
||||
- [ ] icon.ico for webpage
|
||||
- [ ] Record events log
|
||||
- [ ] Hot reload of config.json
|
||||
- [ ] Unit tests
|
||||
- [ ] ReactJS frontend
|
||||
- [ ] Live update of website
|
||||
- [ ] Change settings
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
Light container updating DNS A and/or AAAA records periodically for multiple DNS providers
|
||||
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg">
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions/workflows/build.yml)
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater/tags?page=1&ordering=last_updated)
|
||||

|
||||

|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater/tags)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/commits/main)
|
||||
[](https://github.com/qdm12/ddns-updater/graphs/contributors)
|
||||
[](https://github.com/qdm12/ddns-updater/pulls?q=is%3Apr+is%3Aclosed)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater)
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/master/LICENSE)
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Aliyun
|
||||
- AllInkl
|
||||
- Cloudflare
|
||||
- DD24
|
||||
- DDNSS.de
|
||||
- DigitalOcean
|
||||
- DonDominio
|
||||
- DNSOMatic
|
||||
- DNSPod
|
||||
- Dreamhost
|
||||
- DuckDNS
|
||||
- DynDNS
|
||||
- Dynu
|
||||
- FreeDNS
|
||||
- Gandi
|
||||
- GCP
|
||||
- GoDaddy
|
||||
- Google
|
||||
- He.net
|
||||
- Infomaniak
|
||||
- INWX
|
||||
- Linode
|
||||
- LuaDNS
|
||||
- Namecheap
|
||||
- NoIP
|
||||
- Njalla
|
||||
- OpenDNS
|
||||
- OVH
|
||||
- Porkbun
|
||||
- Selfhost.de
|
||||
- Servercow.de
|
||||
- Spdyn
|
||||
- Strato.de
|
||||
- Variomedia.de
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 11MB Docker image based on a Go static binary in a Scratch Docker image
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/services/overview/) using `SHOUTRRR_ADDRESSES`
|
||||
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
The program reads the configuration from a JSON object, either from a file or from an environment variable.
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
mkdir data
|
||||
touch data/config.json
|
||||
# Owned by user ID of Docker container (1000)
|
||||
chown -R 1000 data
|
||||
# all access (for creating json database file data/updates.json)
|
||||
chmod 700 data
|
||||
# read access only
|
||||
chmod 400 data/config.json
|
||||
```
|
||||
|
||||
If you want to use another user ID, [build the image yourself](#build-the-image) with `--build-arg UID=<your-uid>`. You could also just run the container as root with `--user="0"` but this is not advised security wise.
|
||||
1. Write a JSON configuration in *data/config.json*, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can find more information in the [configuration section](#configuration) to customize it.
|
||||
|
||||
1. Run the container with
|
||||
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
1. ⚠️ If you use IPv6, you might need to set `-e IPV6_PREFIX=/64` (`/64` is your prefix, depending on your ISP)
|
||||
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
|
||||
|
||||
### Next steps
|
||||
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
### GHCR
|
||||
|
||||
Images are also added to the Github Container Registry. To use the GHCR container replace `qmcgaw/ddns-updater` to `ghcr.io/qdm12/ddns-updater`, further details are available [here](https://github.com/qdm12/ddns-updater/pkgs/container/ddns-updater)
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
- [Aliyun](https://github.com/qdm12/ddns-updater/blob/master/docs/aliyun.md)
|
||||
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
|
||||
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
|
||||
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
|
||||
- [DD24](https://github.com/qdm12/ddns-updater/blob/master/docs/domaindiscount24.md)
|
||||
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
|
||||
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
|
||||
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
|
||||
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
|
||||
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
|
||||
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
|
||||
- [Dynu](https://github.com/qdm12/ddns-updater/blob/master/docs/dynu.md)
|
||||
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
|
||||
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
|
||||
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
|
||||
- [GCP](https://github.com/qdm12/ddns-updater/blob/master/docs/gcp.md)
|
||||
- [GoDaddy](https://github.com/qdm12/ddns-updater/blob/master/docs/godaddy.md)
|
||||
- [Google](https://github.com/qdm12/ddns-updater/blob/master/docs/google.md)
|
||||
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
|
||||
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
|
||||
- [INWX](https://github.com/qdm12/ddns-updater/blob/master/docs/inwx.md)
|
||||
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
|
||||
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
|
||||
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
|
||||
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
|
||||
- [Njalla](https://github.com/qdm12/ddns-updater/blob/master/docs/njalla.md)
|
||||
- [OpenDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/opendns.md)
|
||||
- [OVH](https://github.com/qdm12/ddns-updater/blob/master/docs/ovh.md)
|
||||
- [Porkbun](https://github.com/qdm12/ddns-updater/blob/master/docs/porkbun.md)
|
||||
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
|
||||
- [Servercow.de](https://github.com/qdm12/ddns-updater/blob/master/docs/servercow.md)
|
||||
- [Spdyn](https://github.com/qdm12/ddns-updater/blob/master/docs/spdyn.md)
|
||||
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
|
||||
- [Variomedia.de](https://github.com/qdm12/ddns-updater/blob/master/docs/variomedia.md)
|
||||
|
||||
Note that:
|
||||
|
||||
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
|
||||
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `IPV6_PREFIX` | `/128` | IPv6 prefix used to mask your public IPv6 address and your record IPv6 address. Ranges from `/0` to `/128` depending on your ISP. |
|
||||
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
|
||||
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
|
||||
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `HEALTH_SERVER_ADDRESS` | `127.0.0.1:9999` | Health server listening address |
|
||||
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
|
||||
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
|
||||
| `RESOLVER_ADDRESS` | Your network DNS | A plaintext DNS address to use, such as `1.1.1.1:53`. This is useful for split dns, see [#389](https://github.com/qdm12/ddns-updater/issues/389) |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
|
||||
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
|
||||
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/services/overview/) (notification services) |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### Public IP
|
||||
|
||||
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
|
||||
|
||||
On top of that, for each fetching method, all echo services available are cycled on each request.
|
||||
|
||||
This allows you not to be blocked for making too many requests.
|
||||
|
||||
You can otherwise customize it with the following:
|
||||
|
||||
- `PUBLICIP_HTTP_PROVIDERS` gets your public IPv4 or IPv6 address. It can be one or more of the following:
|
||||
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
|
||||
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
|
||||
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
|
||||
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV4_HTTP_PROVIDERS` gets your public IPv4 address only. It can be one or more of the following:
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV6_HTTP_PROVIDERS` gets your public IPv6 address only. It can be one or more of the following:
|
||||
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIP_DNS_PROVIDERS` gets your public IPv4 address only or IPv6 address only or one of them (see #136). It can be one or more of the following:
|
||||
- `google`
|
||||
- `cloudflare`
|
||||
|
||||
### Host firewall
|
||||
|
||||
If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
- TCP 443 outbound for outbound HTTPS
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Architecture
|
||||
|
||||
At program start and every period (5 minutes by default):
|
||||
|
||||
1. Fetch your public IP address
|
||||
1. For each record:
|
||||
1. DNS resolve it to obtain its current IP address(es)
|
||||
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
|
||||
1. Check if your public IP address is within the resolved IP addresses
|
||||
- Yes: skip the update
|
||||
- No: update the record with your public IP address by calling the DNS provider API
|
||||
|
||||
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
|
||||
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
|
||||
|
||||
### Special case: Cloudflare
|
||||
|
||||
For Cloudflare records with the `proxied` option, the following is done.
|
||||
|
||||
At program start and every period (5 minutes by default), for each record:
|
||||
|
||||
1. Fetch your public IP address
|
||||
1. For each record:
|
||||
1. Check the last IP address (persisted in `updates.json`) for that record
|
||||
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
|
||||
1. Check if your public IP address matches the last IP address you updated the record with
|
||||
- Yes: skip the update
|
||||
- No: update the record with your public IP address by calling the DNS provider API
|
||||
|
||||
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
|
||||
|
||||
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
|
||||
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
|
||||
|
||||
## Testing
|
||||
|
||||
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
|
||||
- You can also manually check, by:
|
||||
1. Going to your DNS management webpage
|
||||
1. Setting your record to `127.0.0.1`
|
||||
1. Run the container
|
||||
1. Refresh the DNS management webpage and verify the update happened
|
||||
|
||||
## Build the image
|
||||
|
||||
You can build the image yourself with:
|
||||
|
||||
```sh
|
||||
docker build -t qmcgaw/ddns-updater https://github.com/qdm12/ddns-updater.git
|
||||
```
|
||||
|
||||
You can use optional build arguments with `--build-arg KEY=VALUE` from the table below:
|
||||
|
||||
| Build argument | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `UID` | `1000` | User ID running the container |
|
||||
| `GID` | `1000` | User group ID running the container |
|
||||
| `VERSION` | `unknown` | Version of the program and Docker image |
|
||||
| `BUILD_DATE` | `an unknown date` | Build date of the program and Docker image |
|
||||
| `COMMIT` | `unknown` | Commit hash of the program and Docker image |
|
||||
|
||||
## Development and contributing
|
||||
|
||||
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
|
||||
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
|
||||
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
|
||||
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
|
||||
|
||||
## License
|
||||
|
||||
This repository is under an [MIT license](https://github.com/qdm12/ddns-updater/master/license)
|
||||
|
||||
## Used in external projects
|
||||
|
||||
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
|
||||
|
||||
## Support
|
||||
|
||||
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
|
||||
[](https://github.com/sponsors/qdm12)
|
||||
[](https://www.paypal.me/qmcgaw)
|
||||
|
||||
Many thanks to J. Famiglietti for supporting me financially 🥇👍
|
||||
|
||||
@@ -2,266 +2,303 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/config"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/health"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
jsonparams "github.com/qdm12/ddns-updater/internal/params"
|
||||
persistence "github.com/qdm12/ddns-updater/internal/persistence/json"
|
||||
recordslib "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/golibs/connectivity"
|
||||
"github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/goshutdown"
|
||||
"github.com/qdm12/gosplash"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
version = "unknown"
|
||||
commit = "unknown"
|
||||
buildDate = "an unknown date"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
buildInfo := models.BuildInformation{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: buildDate,
|
||||
}
|
||||
env := params.New()
|
||||
logger := log.New()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
errorCh <- _main(ctx, env, os.Args, logger, buildInfo, time.Now)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stop()
|
||||
logger.Warn("Caught OS signal, shutting down")
|
||||
case err := <-errorCh:
|
||||
stop()
|
||||
close(errorCh)
|
||||
if err == nil { // expected exit such as healthcheck
|
||||
os.Exit(0)
|
||||
}
|
||||
logger.Error(err.Error())
|
||||
cancel()
|
||||
}
|
||||
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case err := <-errorCh:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
logger.Info("Shutdown successful")
|
||||
case <-timer.C:
|
||||
logger.Warn("Shutdown timed out")
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
var (
|
||||
errShoutrrrSetup = errors.New("failed setting up Shoutrrr")
|
||||
)
|
||||
|
||||
func _main(ctx context.Context, env params.Interface, args []string, logger log.LoggerInterface,
|
||||
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
|
||||
if health.IsClientMode(args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
client := health.NewClient()
|
||||
var healthConfig config.Health
|
||||
_, err := healthConfig.Get(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
return client.Query(ctx, healthConfig.Port)
|
||||
}
|
||||
|
||||
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2021-07-22T00:00:00Z")
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
return err
|
||||
}
|
||||
splashSettings := gosplash.Settings{
|
||||
User: "qdm12",
|
||||
Repository: "ddns-updater",
|
||||
Emails: []string{"quentin.mcgaw@gmail.com"},
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
BuildDate: buildInfo.BuildDate,
|
||||
Announcement: "",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
GithubSponsor: "qdm12",
|
||||
}
|
||||
for _, line := range gosplash.MakeLines(splashSettings) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(dataDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
var config config.Config
|
||||
warnings, err := config.Get(env)
|
||||
for _, warning := range warnings {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Setup logger
|
||||
options := []log.Option{log.SetLevel(config.Logger.Level)}
|
||||
if config.Logger.Caller {
|
||||
options = append(options, log.SetCallerFile(true), log.SetCallerLine(true))
|
||||
}
|
||||
logger.Patch(options...)
|
||||
|
||||
sender, err := shoutrrr.CreateSender(config.Shoutrrr.Addresses...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errShoutrrrSetup, err)
|
||||
}
|
||||
notify := func(message string) {
|
||||
errs := sender.Send(message, &config.Shoutrrr.Params)
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
destination := strings.Split(config.Shoutrrr.Addresses[i], ":")[0]
|
||||
logger.Error(destination + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewDatabase(config.Paths.DataDir)
|
||||
if err != nil {
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
jsonReader := jsonparams.NewReader(logger)
|
||||
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON)
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
notify(w)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
|
||||
L := len(settings)
|
||||
switch L {
|
||||
case 0:
|
||||
logger.Warn("Found no setting to update record")
|
||||
case 1:
|
||||
logger.Info("Found single setting to update record")
|
||||
default:
|
||||
logger.Info("Found " + fmt.Sprint(len(settings)) + " settings to update records")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
logger.Warn(err)
|
||||
|
||||
client := &http.Client{Timeout: config.Client.Timeout}
|
||||
|
||||
connectivity := connectivity.NewHTTPSGetChecker(client, http.StatusOK)
|
||||
err = connectivity.Check(ctx, "https://github.com")
|
||||
if err != nil {
|
||||
logger.Warn(err.Error())
|
||||
}
|
||||
records := make([]models.Record, len(settings))
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
i := 0
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
|
||||
records := make([]recordslib.Record, len(settings))
|
||||
for i, s := range settings {
|
||||
logger.Info("Reading history from database: domain " +
|
||||
s.Domain() + " host " + s.Host())
|
||||
events, err := persistentDB.GetEvents(s.Domain(), s.Host())
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
records[i] = models.NewRecord(setting, events)
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
i++
|
||||
records[i] = recordslib.New(s, events)
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
defer client.Close()
|
||||
|
||||
defer client.CloseIdleConnections()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}()
|
||||
updater := update.NewUpdater(db, logger, client, notify)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
checkError := func(err error) {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, checkError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, checkError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
serverErrors := make(chan []error)
|
||||
go func() {
|
||||
serverErrors <- server.RunServers(ctx,
|
||||
server.Settings{Name: "production", Addr: "0.0.0.0:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
|
||||
config.PubIP.HTTPSettings.Client = client
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
os.Interrupt,
|
||||
)
|
||||
select {
|
||||
case errors := <-serverErrors:
|
||||
for _, err := range errors {
|
||||
logger.Error(err)
|
||||
}
|
||||
return 1
|
||||
case signal := <-osSignals:
|
||||
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
|
||||
logger.Warn(message)
|
||||
notify(2, message)
|
||||
return 2
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
return 1
|
||||
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolver, err := resolver.New(config.Resolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating resolver: %w", err)
|
||||
}
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
|
||||
config.IPv6.Mask, config.Update.Cooldown, logger, resolver, timeNow)
|
||||
|
||||
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler("runner")
|
||||
go runner.Run(runnerCtx, runnerDone)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
isHealthy := health.MakeIsHealthy(db, resolver)
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck server"))
|
||||
healthServer := health.NewServer(config.Health.ServerAddress,
|
||||
healthLogger, isHealthy)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler("health server")
|
||||
go healthServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
address := ":" + strconv.Itoa(int(config.Server.Port))
|
||||
serverLogger := logger.New(log.SetComponent("http server"))
|
||||
server := server.New(ctx, address, config.Server.RootURL, db, serverLogger, runner)
|
||||
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler("server")
|
||||
go server.Run(serverCtx, serverDone)
|
||||
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
|
||||
|
||||
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler("backup")
|
||||
backupLogger := logger.New(log.SetComponent("backup"))
|
||||
go backupRunLoop(backupCtx, backupDone, config.Backup.Period, config.Paths.DataDir, config.Backup.Directory,
|
||||
backupLogger, timeNow)
|
||||
|
||||
shutdownGroup := goshutdown.NewGroupHandler("")
|
||||
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
err = shutdownGroup.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupLogger() (logging.Logger, error) {
|
||||
paramsReader := params.NewReader(nil)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logging.NewLogger(encoding, level, nodeID)
|
||||
type InfoErroer interface {
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gotify := admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second})
|
||||
return func(priority int, messageArgs ...interface{}) {
|
||||
if err := gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getParams(paramsReader params.Reader) (
|
||||
dir, dataDir,
|
||||
listeningPort, rootURL string,
|
||||
defaultPeriod time.Duration,
|
||||
backupPeriod time.Duration, backupDirectory string,
|
||||
err error) {
|
||||
dir, err = paramsReader.GetExeDir()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
dataDir, err = paramsReader.GetDataDir(dir)
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
rootURL, err = paramsReader.GetRootURL()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
|
||||
backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
|
||||
dataDir, outputDir string, logger InfoErroer, timeNow func() time.Time) {
|
||||
defer close(done)
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
}
|
||||
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
|
||||
logger.Info("each " + backupPeriod.String() +
|
||||
"; writing zip files to directory " + outputDir)
|
||||
ziper := backup.NewZiper()
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
for {
|
||||
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
|
||||
if err := ziper.ZipFiles(
|
||||
filepath,
|
||||
fmt.Sprintf("%s/data/updates.json", exeDir),
|
||||
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
|
||||
logger.Error(err)
|
||||
fileName := "ddns-updater-backup-" + strconv.Itoa(int(timeNow().UnixNano())) + ".zip"
|
||||
zipFilepath := filepath.Join(outputDir, fileName)
|
||||
err := ziper.ZipFiles(
|
||||
zipFilepath,
|
||||
filepath.Join(dataDir, "updates.json"),
|
||||
filepath.Join(dataDir, "config.json"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
|
||||
@@ -4,28 +4,23 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "google",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
},
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "example.info",
|
||||
"ip_method": "opendns",
|
||||
"key": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,15 +9,27 @@ services:
|
||||
volumes:
|
||||
- ./data:/updater/data
|
||||
environment:
|
||||
- DELAY=300s
|
||||
- ROOT_URL=/
|
||||
- LISTENING_PORT=8000
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=0
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- UPDATE_COOLDOWN_PERIOD=5m
|
||||
- PUBLICIP_FETCHERS=all
|
||||
- PUBLICIP_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV4_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV6_HTTP_PROVIDERS=all
|
||||
- PUBLICIP_DNS_PROVIDERS=all
|
||||
- PUBLICIP_DNS_TIMEOUT=3s
|
||||
- HTTP_TIMEOUT=10s
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
- BACKUP_PERIOD=0
|
||||
|
||||
# Web UI
|
||||
- LISTENING_PORT=8000
|
||||
- ROOT_URL=/
|
||||
|
||||
# Backup
|
||||
- BACKUP_PERIOD=0 # 0 to disable
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_LEVEL=info
|
||||
- LOG_CALLER=hidden
|
||||
- SHOUTRRR_ADDRESSES=
|
||||
restart: always
|
||||
|
||||
33
docs/aliyun.md
Normal file
33
docs/aliyun.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Aliyun
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "aliyun",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"access_key_id": "your access_key_id",
|
||||
"access_secret": "your access_secret",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"access_key_id"`
|
||||
- `"access_secret"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
28
docs/allinkl.md
Normal file
28
docs/allinkl.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# All-Inkl
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "allinkl",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"username": "dynXXXXXXX",
|
||||
"password": "password"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"username"` username (usually starts with dyn followed by numbers)
|
||||
- `"password"` password in plain text
|
||||
|
||||
## Domain setup
|
||||
40
docs/cloudflare.md
Normal file
40
docs/cloudflare.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Cloudflare
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "cloudflare",
|
||||
"zone_identifier": "some id",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site, from the domain overview page written as *Zone ID*
|
||||
- `"domain"`
|
||||
- `"host"` is your host. It should be left to `"@"`, since subdomain and wildcards (`"*"`) are not really supported by Cloudflare it seems.
|
||||
See [this issue comment for context](https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949). This is left as is for compatibility.
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following ([how to find API keys](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)):
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"proxied"` can be set to `true` to use the proxy services of Cloudflare
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
52
docs/contributing.md
Normal file
52
docs/contributing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
|
||||
## Table of content
|
||||
|
||||
1. [Setup](#Setup)
|
||||
1. [Commands available](#Commands-available)
|
||||
1. [Guidelines](#Guidelines)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](../.vscode/settings.json).
|
||||
|
||||
## Build and Run
|
||||
|
||||
```sh
|
||||
go build -o app cmd/updater/main.go
|
||||
./app
|
||||
```
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Guidelines
|
||||
|
||||
The Go code is in the Go file [cmd/updater/main.go](../cmd/updater/main.go) and the [internal directory](../internal), you might want to start reading the main.go file.
|
||||
|
||||
See the [Contributing document](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
29
docs/dd24.md
Normal file
29
docs/dd24.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Domain Discount 24
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dd24",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"password"` is your password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
38
docs/ddnss.de.md
Normal file
38
docs/ddnss.de.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# DDNSS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ddnss",
|
||||
"provider_ip": true,
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "user",
|
||||
"password": "password",
|
||||
"dual_stack": false,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"dual_stack"` can be set to `true` **if you have turn on dual stack for your record** to update both IPv4 and IPv6 addresses. Note it is ignored if `"provider_ip": true`. More precisely:
|
||||
- if it is `false`, the updates are done using the `ip` parameter and only one IP address can be set (ipv4 or ipv6, whichever is last sent).
|
||||
- if it is `true`, the updates are done using the `ip` and `ip6` parameters, for IPv4 and IPv6 respectively, and both can be set on the same record
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/digitalocean.md
Normal file
31
docs/digitalocean.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Digital Ocean
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "digitalocean",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"` is your token that you can create [here](https://cloud.digitalocean.com/settings/applications)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
32
docs/dnsomatic.md
Normal file
32
docs/dnsomatic.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# DNS-O-Matic
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnsomatic",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/dnspod.md
Normal file
31
docs/dnspod.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# DNSPod
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnspod",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
33
docs/dondominio.md
Normal file
33
docs/dondominio.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Don Dominio
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dondominio",
|
||||
"domain": "domain.com",
|
||||
"name": "something",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"name"` is the name server associated with the domain
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/dreamhost.md
Normal file
31
docs/dreamhost.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Dreamhost
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`. It defaults to `"@"`.
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
35
docs/duckdns.md
Normal file
35
docs/duckdns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DuckDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"host"` is your host, for example `subdomain` for `subdomain.duckdns.org`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (**NOT** your IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See the [duckdns website](https://duckdns.org)*
|
||||
35
docs/dyndns.md
Normal file
35
docs/dyndns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DynDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"client_key": "client_key",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"client_key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
37
docs/dynu.md
Normal file
37
docs/dynu.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Dynu
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dynu",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"group": "group",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"` could be plain text or password in MD5 or SHA256 format (There's also an option for setting a password for IP Update only)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
- `"group"` specify the Group for which you want to set the IP (will update any domains and subdomains in the same group)
|
||||
|
||||
## Domain setup
|
||||
33
docs/dynv6.md
Normal file
33
docs/dynv6.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# DynV6
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dynv6",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"` that you can obtain [here](https://dynv6.com/keys#token)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
33
docs/freedns.md
Normal file
33
docs/freedns.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# FreeDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "freedns",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"token"` is the randomized update token you use to update your record
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
This integration uses FreeDNS's v2 dynamic dns interface, which is not shown by default when you select `Dynamic DNS` from the side menu. Instead you must go to https://freedns.afraid.org/dynamic/v2/ and enable dynamic DNS for the subdomains you wish and you will then see a url like `https://sync.afraid.org/u/token/` for each enabled subdomain.
|
||||
37
docs/gandi.md
Normal file
37
docs/gandi.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Gandi
|
||||
|
||||
This provider uses Gandi v5 API
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "gandi",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ttl": 3600,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` which can be a subdomain, `@` or a wildcard `*`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"ttl"` default is `3600`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[Gandi Documentation Website](https://docs.gandi.net/en/domain_names/advanced_users/api.html#gandi-s-api)
|
||||
37
docs/gcp.md
Normal file
37
docs/gcp.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# GCP
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "gpc",
|
||||
"project": "my-project-id",
|
||||
"zone": "zone",
|
||||
"credentials": {
|
||||
"type": "service_account",
|
||||
"project_id": "my-project-id",
|
||||
// ...
|
||||
},
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"project"` is the id of your Google Cloud project
|
||||
- `"zone"` is the zone, that your DNS record is located in
|
||||
- `"credentials"` is the JSON credentials for your Google Cloud project. This is usually downloaded as a JSON file, which you can copy paste the content as the value of the `"credentials"` key. More information on how to get it is available [here](https://cloud.google.com/docs/authentication/getting-started). Please ensure your service account has all necessary permissions to create/update/list/get DNS records within your project.
|
||||
- `"domain"` is the TLD of you DNS record (without a trailing dot)
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4`
|
||||
61
docs/godaddy.md
Normal file
61
docs/godaddy.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# GoDaddy
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Go to [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
|
||||
1. Change the IP address to `127.0.0.1`
|
||||
1. Run the ddns-updater
|
||||
1. Refresh the Godaddy webpage to check the update occurred.
|
||||
42
docs/google.md
Normal file
42
docs/google.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Google
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "google",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
Thanks to [@gauravspatel](https://github.com/gauravspatel) for #124
|
||||
|
||||
1. Enable dynamic DNS in the *synthetic records* section of DNS management.
|
||||
1. The username and password is generated once you create the dynamic DNS entry.
|
||||
|
||||
### Wildcard entries
|
||||
|
||||
If you want to create a **wildcard entry**, you have to create a custom **CNAME** record with key `"*"` and value `"@"`.
|
||||
31
docs/he.net.md
Normal file
31
docs/he.net.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# He.net
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "he",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
34
docs/infomaniak.md
Normal file
34
docs/infomaniak.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Infomaniak
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "infomaniak",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
32
docs/inwx.md
Normal file
32
docs/inwx.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OpenDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
34
docs/linode.md
Normal file
34
docs/linode.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Linode
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "linode",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Create a personal access token with `domains` set, with read and write privileges, ideally that never expires. You can refer to [@AnujRNair's comment](https://github.com/qdm12/ddns-updater/pull/144#discussion_r559292678) and to [Linode's guide](https://www.linode.com/docs/products/tools/cloud-manager/guides/cloud-api-keys).
|
||||
1. The program will create the A or AAAA record for you if it doesn't exist already.
|
||||
37
docs/luadns.md
Normal file
37
docs/luadns.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# LuaDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "luadns",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"email": "email",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"email"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Go to [api.luadns.com/settings](https://api.luadns.com/settings)
|
||||
1. Enable API access
|
||||
1. Obtain your API token and replace it in the parameters as the value for `token`
|
||||
57
docs/namecheap.md
Normal file
57
docs/namecheap.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Namecheap
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
Note that Namecheap only supports ipv4 addresses for now.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
|
||||

|
||||
|
||||
1. Select the following settings and create the *A + Dynamic DNS Record*:
|
||||
|
||||

|
||||
|
||||
1. Scroll down and turn on the switch for *DYNAMIC DNS*
|
||||
|
||||

|
||||
|
||||
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
|
||||
|
||||

|
||||
35
docs/njalla.md
Normal file
35
docs/njalla.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Njalla
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "njalla",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"key"` is the key for your record
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [https://njal.la/docs/ddns](https://njal.la/docs/ddns/)
|
||||
34
docs/noip.md
Normal file
34
docs/noip.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# NoIP
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "noip",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
35
docs/opendns.md
Normal file
35
docs/opendns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# OpenDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
51
docs/ovh.md
Normal file
51
docs/ovh.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# OVH
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ovh",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
|
||||
#### Using DynHost
|
||||
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
#### OR Using ZoneDNS
|
||||
|
||||
- `"api_endpoint"` default value is `"ovh-eu"`
|
||||
- `"app_key"` which you can create at [eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp/)
|
||||
- `"app_secret"`
|
||||
- `"consumer_key"`
|
||||
|
||||
The ZoneDNS implementation allows you to update any record name including *.yourdomain.tld
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
|
||||
|
||||
## Domain setup
|
||||
|
||||
- If you use DynHost: [docs.ovh.com/ie/en/domains/hosting_dynhost](https://docs.ovh.com/ie/en/domains/hosting_dynhost/)
|
||||
- If you use the ZoneDNS API: [docs.ovh.com/gb/en/customer/first-steps-with-ovh-api](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)
|
||||
35
docs/porkbun.md
Normal file
35
docs/porkbun.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Porkbun
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "porkbun",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"api_key": "sk1_7d119e3f656b00ae042980302e1425a04163c476efec1833q3cb0w54fc6f5022",
|
||||
"secret_api_key": "pk1_5299b57125c8f3cdf347d2fe0e713311ee3a1e11f11a14942b26472593e35368",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"*"` or `"@"`
|
||||
- `"apikey"`
|
||||
- `"secretapikey"`
|
||||
- `"ttl"` optional integer value corresponding to a number of seconds
|
||||
|
||||
## Domain setup
|
||||
|
||||
- Create an API key at [porkbun.com/account/api](https://porkbun.com/account/api)
|
||||
- From the [Domain Management page](https://porkbun.com/account/domainsSpeedy), toggle on **API ACCESS** for your domain.
|
||||
|
||||
💁 [Official setup documentation](https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-dns-api)
|
||||
33
docs/selfhost.de.md
Normal file
33
docs/selfhost.de.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Selfhost.de
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "selfhost.de",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"` is your DynDNS username
|
||||
- `"password"` is your DynDNS password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
37
docs/servercow.md
Normal file
37
docs/servercow.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Servercow
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "servercow",
|
||||
"domain": "domain.com",
|
||||
"host": "",
|
||||
"username": "servercow_username",
|
||||
"password": "servercow_password",
|
||||
"ttl": 600,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsury parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be `""`, a subdomain or `"*"` generally
|
||||
- `"username"` is the username for your DNS API User
|
||||
- `"password"` is the password for your DNS API User
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ttl"` can be set to an integer value for record TTL in seconds (if not set the default is 120)
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [their article](https://cp.servercow.de/en/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/)
|
||||
41
docs/spdyn.md
Normal file
41
docs/spdyn.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Spdyn.de
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "spdyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"user": "user",
|
||||
"password": "password",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
|
||||
#### Using user and password
|
||||
|
||||
- `"user"` is the name of a user who can update this host
|
||||
- `"password"` is the password of a user who can update this host
|
||||
|
||||
#### Using update tokens
|
||||
|
||||
- `"token"` is your update token
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (**not IPv6**)automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
35
docs/strato.md
Normal file
35
docs/strato.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Strato
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "strato",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"password"` is your dyndns password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [their article](https://www.strato.com/faq/en_us/domain/this-is-how-easy-it-is-to-set-up-dyndns-for-your-domains/)
|
||||
37
docs/variomedia.md
Normal file
37
docs/variomedia.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Variomedia
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "variomedia",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"email": "email@domain.com",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"email"`
|
||||
- `"password"` is your DNS settings password, not your account password ⚠️
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [dyndns.variomedia.de](https://dyndns.variomedia.de/)
|
||||
44
go.mod
44
go.mod
@@ -1,11 +1,43 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.13
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/breml/rootcerts v0.2.11
|
||||
github.com/containrrr/shoutrrr v0.7.0
|
||||
github.com/go-chi/chi v1.5.4
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/miekg/dns v1.1.42
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.1.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
google.golang.org/api v0.102.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -6,25 +6,27 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Ziper interface {
|
||||
var _ FileZiper = (*Ziper)(nil)
|
||||
|
||||
type FileZiper interface {
|
||||
ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
}
|
||||
|
||||
type ziper struct {
|
||||
type Ziper struct {
|
||||
createFile func(name string) (*os.File, error)
|
||||
openFile func(name string) (*os.File, error)
|
||||
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
func NewZiper() Ziper {
|
||||
return &ziper{
|
||||
func NewZiper() *Ziper {
|
||||
return &Ziper{
|
||||
createFile: os.Create,
|
||||
openFile: os.Open,
|
||||
ioCopy: io.Copy,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := z.createFile(outputFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -33,14 +35,15 @@ func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
for _, filepath := range inputFilepaths {
|
||||
if err := z.addFile(w, filepath); err != nil {
|
||||
err = z.addFile(w, filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
func (z *Ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := z.openFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
27
internal/config/backup.go
Normal file
27
internal/config/backup.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Period time.Duration
|
||||
Directory string
|
||||
}
|
||||
|
||||
func (b *Backup) get(env params.Interface) (err error) {
|
||||
b.Period, err = env.Duration("BACKUP_PERIOD", params.Default("0"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable BACKUP_PERIOD", err)
|
||||
}
|
||||
|
||||
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable BACKUP_DIRECTORY", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
internal/config/client.go
Normal file
21
internal/config/client.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Client) get(env params.Interface) (err error) {
|
||||
c.Timeout, err = env.Duration("HTTP_TIMEOUT", params.Default("10s"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable HTTP_TIMEOUT", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
86
internal/config/config.go
Normal file
86
internal/config/config.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Client Client
|
||||
Update Update
|
||||
PubIP PubIP
|
||||
Resolver resolver.Settings
|
||||
IPv6 IPv6
|
||||
Server Server
|
||||
Health Health
|
||||
Paths Paths
|
||||
Backup Backup
|
||||
Logger Logger
|
||||
Shoutrrr Shoutrrr
|
||||
}
|
||||
|
||||
func (c *Config) Get(env params.Interface) (warnings []string, err error) {
|
||||
err = c.Client.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err := c.Update.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err := c.PubIP.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
c.Resolver, err = readResolver()
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("reading resolver settings: %w", err)
|
||||
}
|
||||
|
||||
err = c.IPv6.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Server.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Health.Get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
err = c.Paths.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
err = c.Backup.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
c.Logger, err = readLog()
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err = c.Shoutrrr.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
32
internal/config/health.go
Normal file
32
internal/config/health.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
ServerAddress string
|
||||
Port uint16 // obtained from ServerAddress
|
||||
}
|
||||
|
||||
func (h *Health) Get(env params.Interface) (warning string, err error) {
|
||||
h.ServerAddress, warning, err = env.ListeningAddress(
|
||||
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
_, portStr, err := net.SplitHostPort(h.ServerAddress)
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
h.Port = uint16(port)
|
||||
return warning, nil
|
||||
}
|
||||
60
internal/config/ipv6.go
Normal file
60
internal/config/ipv6.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type IPv6 struct {
|
||||
Mask net.IPMask
|
||||
}
|
||||
|
||||
func (i *IPv6) get(env params.Interface) (err error) {
|
||||
maskStr, err := env.Get("IPV6_PREFIX", params.Default("/128"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
|
||||
}
|
||||
i.Mask, err = ipv6DecimalPrefixToMask(maskStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrParsePrefix = errors.New("cannot parse IP prefix")
|
||||
|
||||
func ipv6DecimalPrefixToMask(prefixDecimal string) (ipMask net.IPMask, err error) {
|
||||
if prefixDecimal == "" {
|
||||
return nil, fmt.Errorf("%w: empty prefix", ErrParsePrefix)
|
||||
}
|
||||
|
||||
prefixDecimal = strings.TrimPrefix(prefixDecimal, "/")
|
||||
|
||||
const bits = 8 * net.IPv6len
|
||||
|
||||
ones, consumed, ok := decimalToInteger(prefixDecimal)
|
||||
if !ok || consumed != len(prefixDecimal) || ones < 0 || ones > bits {
|
||||
return nil, fmt.Errorf("%w: %s", ErrParsePrefix, prefixDecimal)
|
||||
}
|
||||
|
||||
return net.CIDRMask(ones, bits), nil
|
||||
}
|
||||
|
||||
func decimalToInteger(s string) (ones int, i int, ok bool) {
|
||||
const big = 0xFFFFFF // Bigger than we need, not too big to worry about overflow
|
||||
const ten = 10
|
||||
|
||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
ones = ones*ten + int(s[i]-'0')
|
||||
if ones >= big {
|
||||
return big, i, false
|
||||
}
|
||||
}
|
||||
|
||||
return ones, i, true
|
||||
}
|
||||
62
internal/config/ipv6_test.go
Normal file
62
internal/config/ipv6_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ipv6DecimalPrefixToMask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
prefixDecimal string
|
||||
ipMask net.IPMask
|
||||
err error
|
||||
}{
|
||||
"empty": {
|
||||
err: fmt.Errorf("cannot parse IP prefix: empty prefix"),
|
||||
},
|
||||
"malformed": {
|
||||
prefixDecimal: "malformed",
|
||||
err: fmt.Errorf("cannot parse IP prefix: malformed"),
|
||||
},
|
||||
"with leading slash": {
|
||||
prefixDecimal: "/78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"without leading slash": {
|
||||
prefixDecimal: "78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"full IPv6 mask": {
|
||||
prefixDecimal: "/128",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
|
||||
},
|
||||
"zero IPv6 mask": {
|
||||
prefixDecimal: "/0",
|
||||
ipMask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipMask, err := ipv6DecimalPrefixToMask(testCase.prefixDecimal)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.ipMask, ipMask)
|
||||
})
|
||||
}
|
||||
}
|
||||
73
internal/config/logger.go
Normal file
73
internal/config/logger.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
Caller bool
|
||||
Level log.Level
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLogCallerNotValid = errors.New("LOG_CALLER value is not valid")
|
||||
)
|
||||
|
||||
func readLog() (settings Logger, err error) {
|
||||
callerString := os.Getenv("LOG_CALLER")
|
||||
switch callerString {
|
||||
case "":
|
||||
case "hidden":
|
||||
case "short":
|
||||
settings.Caller = true
|
||||
default:
|
||||
return settings, fmt.Errorf("%w: "+
|
||||
`%q must be one of "", "hidden" or "short"`,
|
||||
ErrLogCallerNotValid, callerString)
|
||||
}
|
||||
|
||||
settings.Level, err = readLogLevel()
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func readLogLevel() (level log.Level, err error) {
|
||||
s := os.Getenv("LOG_LEVEL")
|
||||
if s == "" {
|
||||
return log.LevelInfo, nil
|
||||
}
|
||||
|
||||
level, err = parseLogLevel(s)
|
||||
if err != nil {
|
||||
return level, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
|
||||
}
|
||||
|
||||
return level, nil
|
||||
}
|
||||
|
||||
var ErrLogLevelUnknown = errors.New("log level is unknown")
|
||||
|
||||
func parseLogLevel(s string) (level log.Level, err error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "debug":
|
||||
return log.LevelDebug, nil
|
||||
case "info":
|
||||
return log.LevelInfo, nil
|
||||
case "warning":
|
||||
return log.LevelWarn, nil
|
||||
case "error":
|
||||
return log.LevelError, nil
|
||||
default:
|
||||
return level, fmt.Errorf(
|
||||
"%w: %q is not valid and can be one of debug, info, warning or error",
|
||||
ErrLogLevelUnknown, s)
|
||||
}
|
||||
}
|
||||
23
internal/config/paths.go
Normal file
23
internal/config/paths.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Paths struct {
|
||||
DataDir string
|
||||
JSON string // obtained from DataDir
|
||||
}
|
||||
|
||||
func (p *Paths) get(env params.Interface) (err error) {
|
||||
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable DATADIR", err)
|
||||
}
|
||||
|
||||
p.JSON = filepath.Join(p.DataDir, "config.json")
|
||||
return nil
|
||||
}
|
||||
197
internal/config/pubip.go
Normal file
197
internal/config/pubip.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
const all = "all"
|
||||
|
||||
type PubIP struct {
|
||||
HTTPSettings publicip.HTTPSettings
|
||||
DNSSettings publicip.DNSSettings
|
||||
}
|
||||
|
||||
func (p *PubIP) get(env params.Interface) (warnings []string, err error) {
|
||||
err = p.getFetchers(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
p.HTTPSettings.Options = []http.Option{
|
||||
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
http.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
http.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
dnsIPProviders, err := p.getDNSProviders(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
dnsTimeout, err := env.Duration("PUBLICIP_DNS_TIMEOUT", params.Default("3s"))
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
p.DNSSettings.Options = []dns.Option{
|
||||
dns.SetTimeout(dnsTimeout),
|
||||
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
|
||||
|
||||
func (p *PubIP) getFetchers(env params.Interface) (err error) {
|
||||
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable PUBLICIP_FETCHERS", err)
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
for i, field := range fields {
|
||||
switch strings.ToLower(field) {
|
||||
case all:
|
||||
p.HTTPSettings.Enabled = true
|
||||
p.DNSSettings.Enabled = true
|
||||
case "http":
|
||||
p.HTTPSettings.Enabled = true
|
||||
case "dns":
|
||||
p.DNSSettings.Enabled = true
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"%w: %q at position %d of %d",
|
||||
ErrInvalidFetcher, field, i+1, len(fields))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getDNSProviders obtains the DNS providers to obtain your public IPv4 and/or IPv6 address.
|
||||
func (p *PubIP) getDNSProviders(env params.Interface) (providers []dns.Provider, err error) {
|
||||
s, err := env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable PUBLICIP_DNS_PROVIDERS", err)
|
||||
}
|
||||
|
||||
availableProviders := dns.ListProviders()
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
providers = make([]dns.Provider, len(fields))
|
||||
for i, field := range fields {
|
||||
if field == all {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
providers[i] = dns.Provider(field)
|
||||
err = dns.ValidateProvider(providers[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
|
||||
func (p *PubIP) getIPHTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
// getIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
|
||||
func (p *PubIP) getIPv4HTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
// getIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
|
||||
func (p *PubIP) getIPv6HTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
|
||||
)
|
||||
|
||||
func httpIPMethod(env params.Interface, envKey, retroKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
retroKeyOption := params.RetroKeys([]string{retroKey}, func(oldKey, newKey string) {
|
||||
warning = "You are using an old environment variable " + oldKey +
|
||||
" please change it to " + newKey
|
||||
})
|
||||
s, err := env.Get(envKey, params.Default("cycle"), retroKeyOption)
|
||||
if err != nil {
|
||||
return nil, warning, fmt.Errorf("%w: for environment variable %s", err, envKey)
|
||||
}
|
||||
|
||||
availableProviders := http.ListProvidersForVersion(version)
|
||||
choices := make(map[http.Provider]struct{}, len(availableProviders))
|
||||
for _, provider := range availableProviders {
|
||||
choices[provider] = struct{}{}
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
|
||||
for _, field := range fields {
|
||||
// Retro-compatibility.
|
||||
switch field {
|
||||
case "ipify6":
|
||||
field = "ipify"
|
||||
case "noip4", "noip6", "noip8245_4", "noip8245_6":
|
||||
field = "noip"
|
||||
case "cycle":
|
||||
field = all
|
||||
}
|
||||
|
||||
if field == all {
|
||||
return availableProviders, warning, nil
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(field)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
providers = append(providers, http.CustomProvider(url))
|
||||
continue
|
||||
}
|
||||
|
||||
provider := http.Provider(field)
|
||||
if _, ok := choices[provider]; !ok {
|
||||
return nil, warning, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, warning, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
|
||||
}
|
||||
|
||||
return providers, warning, nil
|
||||
}
|
||||
27
internal/config/resolver.go
Normal file
27
internal/config/resolver.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
)
|
||||
|
||||
func readResolver() (settings resolver.Settings, err error) {
|
||||
address := os.Getenv("RESOLVER_ADDRESS")
|
||||
if address != "" {
|
||||
settings.Address = &address
|
||||
}
|
||||
|
||||
timeoutString := os.Getenv("RESOLVER_TIMEOUT")
|
||||
if timeoutString != "" {
|
||||
timeout, err := time.ParseDuration(timeoutString)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("environment variable RESOLVER_TIMEOUT: %w", err)
|
||||
}
|
||||
settings.Timeout = timeout
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
26
internal/config/server.go
Normal file
26
internal/config/server.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Port uint16
|
||||
RootURL string
|
||||
}
|
||||
|
||||
func (s *Server) get(env params.Interface) (warning string, err error) {
|
||||
s.RootURL, err = env.RootURL("ROOT_URL")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable ROOT_URL", err)
|
||||
}
|
||||
|
||||
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable LISTENING_PORT", err)
|
||||
}
|
||||
|
||||
return warning, err
|
||||
}
|
||||
71
internal/config/shoutrrr.go
Normal file
71
internal/config/shoutrrr.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Shoutrrr struct {
|
||||
Addresses []string
|
||||
Params types.Params
|
||||
}
|
||||
|
||||
func (s *Shoutrrr) get(env params.Interface) (warnings []string, err error) {
|
||||
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES", params.CaseSensitiveValue())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable SHOUTRRR_ADDRESSES", err)
|
||||
}
|
||||
|
||||
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
|
||||
gotifyURL, err := env.URL("GOTIFY_URL")
|
||||
if err != nil || gotifyURL != nil {
|
||||
const warning = "You should use the environment variable SHOUTRRR_ADDRESSES instead of GOTIFY_URL and GOTIFY_TOKEN"
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable GOTIFY_URL", err)
|
||||
} else if gotifyURL != nil {
|
||||
gotifyToken, err := env.Get("GOTIFY_TOKEN", params.CaseSensitiveValue(),
|
||||
params.Compulsory(), params.Unset())
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
|
||||
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
|
||||
}
|
||||
|
||||
_, err = shoutrrr.CreateSender(s.Addresses...)
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("for environment variable SHOUTRRR_ADDRESSES: %w", err) // validation step
|
||||
}
|
||||
|
||||
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"), params.CaseSensitiveValue())
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("%w: for environment variable SHOUTRRR_PARAMS", err)
|
||||
}
|
||||
|
||||
keyValues := strings.Split(str, ",")
|
||||
s.Params = make(map[string]string, len(keyValues))
|
||||
for _, keyValue := range keyValues {
|
||||
fields := strings.Split(keyValue, "=")
|
||||
key, value := fields[0], fields[1]
|
||||
s.Params[key] = value
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
|
||||
hostAndPath := path.Join(url.Host, url.Path)
|
||||
address = "gotify://" + hostAndPath + "/" + token
|
||||
if url.Scheme == "http" {
|
||||
address += "?DisableTLS=Yes"
|
||||
}
|
||||
return address
|
||||
}
|
||||
55
internal/config/update.go
Normal file
55
internal/config/update.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Period time.Duration
|
||||
Cooldown time.Duration
|
||||
}
|
||||
|
||||
func (u *Update) get(env params.Interface) (warning string, err error) {
|
||||
warning, err = u.getPeriod(env)
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
|
||||
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable UPDATE_COOLDOWN_PERIOD", err)
|
||||
}
|
||||
|
||||
return warning, nil
|
||||
}
|
||||
|
||||
func (u *Update) getPeriod(env params.Interface) (warning string, err error) {
|
||||
// Backward compatibility: DELAY
|
||||
s, err := env.Get("DELAY", params.Compulsory())
|
||||
if err == nil {
|
||||
warning = "the environment variable DELAY should be changed to PERIOD"
|
||||
// Backward compatibility: integer only, treated as seconds
|
||||
n, err := strconv.Atoi(s)
|
||||
if err == nil {
|
||||
u.Period = time.Duration(n) * time.Second
|
||||
return warning, nil
|
||||
}
|
||||
|
||||
period, err := time.ParseDuration(s)
|
||||
if err == nil {
|
||||
u.Period = period
|
||||
return warning, nil
|
||||
}
|
||||
}
|
||||
|
||||
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable PERIOD", err)
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
8
internal/config/utils.go
Normal file
8
internal/config/utils.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
|
||||
if s == "" {
|
||||
return slice
|
||||
}
|
||||
return append(slice, s)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
HTMLFail models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTMLSuccess models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTMLUpdate models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTMLUpdating models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO have a struct model containing URL, name for each provider
|
||||
HTMLNamecheap models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTMLGodaddy models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTMLDuckDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTMLDreamhost models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTMLCloudflare models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTMLNoIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTMLDNSPod models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTMLInfomaniak models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTMLDdnssde models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
)
|
||||
|
||||
const (
|
||||
HTMLGoogle models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTMLOpenDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTMLIfconfig models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTMLIpinfo models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTMLIpify models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTMLIpify6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTMLDdnss models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTMLDdnss4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTMLDdnss6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTMLCycle models.HTML = "Cycling"
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
)
|
||||
@@ -1,53 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER models.IPMethod = "provider"
|
||||
OPENDNS models.IPMethod = "opendns"
|
||||
IFCONFIG models.IPMethod = "ifconfig"
|
||||
IPINFO models.IPMethod = "ipinfo"
|
||||
IPIFY models.IPMethod = "ipify"
|
||||
IPIFY6 models.IPMethod = "ipify6"
|
||||
CYCLE models.IPMethod = "cycle"
|
||||
DDNSS models.IPMethod = "ddnss"
|
||||
DDNSS4 models.IPMethod = "ddnss4"
|
||||
DDNSS6 models.IPMethod = "ddnss6"
|
||||
// Retro compatibility only
|
||||
GOOGLE models.IPMethod = "google"
|
||||
)
|
||||
|
||||
func IPMethodMapping() map[models.IPMethod]string {
|
||||
return map[models.IPMethod]string{
|
||||
PROVIDER: string(PROVIDER),
|
||||
CYCLE: string(CYCLE),
|
||||
OPENDNS: "https://diagnostic.opendns.com/myip",
|
||||
IFCONFIG: "https://ifconfig.io/ip",
|
||||
IPINFO: "https://ipinfo.io/ip",
|
||||
IPIFY: "https://api.ipify.org",
|
||||
IPIFY6: "https://api6.ipify.org",
|
||||
DDNSS: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS4: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS6: "https://ip6.ddnss.de/meineip.php",
|
||||
}
|
||||
}
|
||||
|
||||
func IPMethodChoices() (choices []models.IPMethod) {
|
||||
for choice := range IPMethodMapping() {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
func IPMethodExternalChoices() (choices []models.IPMethod) {
|
||||
for _, choice := range IPMethodChoices() {
|
||||
switch choice {
|
||||
case PROVIDER, CYCLE:
|
||||
default:
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
}
|
||||
return choices
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IPMethodChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "provider", "cycle", "opendns", "ifconfig", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
|
||||
func Test_IPMethodExternalChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodExternalChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "ifconfig", "opendns", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values
|
||||
const (
|
||||
GODADDY models.Provider = "godaddy"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
NOIP models.Provider = "noip"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
)
|
||||
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
GODADDY,
|
||||
NAMECHEAP,
|
||||
DUCKDNS,
|
||||
DREAMHOST,
|
||||
CLOUDFLARE,
|
||||
NOIP,
|
||||
DNSPOD,
|
||||
INFOMANIAK,
|
||||
DDNSSDE,
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
goDaddyKey = `[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}` // #nosec
|
||||
duckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}` // #nosec
|
||||
namecheapPassword = `[a-f0-9]{32}` // #nosec
|
||||
dreamhostKey = `[a-zA-Z0-9]{16}`
|
||||
cloudflareKey = `[a-zA-Z0-9]+`
|
||||
cloudflareUserServiceKey = `v1\.0.+`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}` // #nosec
|
||||
)
|
||||
|
||||
func MatchGodaddyKey(s string) bool {
|
||||
return regexp.MustCompile("^" + goDaddyKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchGodaddySecret(s string) bool {
|
||||
return regexp.MustCompile("^" + godaddySecret + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDuckDNSToken(s string) bool {
|
||||
return regexp.MustCompile("^" + duckDNSToken + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchNamecheapPassword(s string) bool {
|
||||
return regexp.MustCompile("^" + namecheapPassword + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDreamhostKey(s string) bool {
|
||||
return regexp.MustCompile("^" + dreamhostKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareUserServiceKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareUserServiceKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareToken(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareToken + "$").MatchString(s)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Announcement is a message announcement
|
||||
Announcement = "Smaller Docker image based on Scratch (12.3MB)"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
AnnouncementExpiration = "2020-04-20"
|
||||
)
|
||||
|
||||
const (
|
||||
// IssueLink is the link for users to use to create issues
|
||||
IssueLink = "https://github.com/qdm12/ddns-updater/issues/new"
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AnnouncementExpiration(t *testing.T) {
|
||||
t.Parallel()
|
||||
if len(AnnouncementExpiration) == 0 {
|
||||
return
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", AnnouncementExpiration)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
SUCCESS models.Status = "success"
|
||||
UPTODATE models.Status = "up to date"
|
||||
UPDATING models.Status = "updating"
|
||||
UNSET models.Status = "unset"
|
||||
)
|
||||
|
||||
@@ -3,36 +3,19 @@ package data
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
Close() error
|
||||
Insert(record models.Record) (id int)
|
||||
Select(id int) (record models.Record, err error)
|
||||
SelectAll() (records []models.Record)
|
||||
Update(id int, record models.Record) error
|
||||
// From persistence database
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
type database struct {
|
||||
data []models.Record
|
||||
type Database struct {
|
||||
data []records.Record
|
||||
sync.RWMutex
|
||||
persistentDB persistence.Database
|
||||
persistentDB PersistentDatabase
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
func NewDatabase(data []models.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
// NewDatabase creates a new in memory database.
|
||||
func NewDatabase(data []records.Record, persistentDB PersistentDatabase) *Database {
|
||||
return &Database{
|
||||
data: data,
|
||||
persistentDB: persistentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
db.Lock() // ensure write operation finishes
|
||||
defer db.Unlock()
|
||||
return db.persistentDB.Close()
|
||||
}
|
||||
|
||||
15
internal/data/interfaces.go
Normal file
15
internal/data/interfaces.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type PersistentDatabase interface {
|
||||
Close() error
|
||||
StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error)
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
Check() error
|
||||
}
|
||||
@@ -1,31 +1,24 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) Insert(record models.Record) (id int) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
db.data = append(db.data, record)
|
||||
return len(db.data) - 1
|
||||
}
|
||||
var ErrRecordNotFound = errors.New("record not found")
|
||||
|
||||
func (db *database) Select(id int) (record models.Record, err error) {
|
||||
func (db *Database) Select(id uint) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
if id < 0 {
|
||||
return record, fmt.Errorf("id %d cannot be lower than 0", id)
|
||||
}
|
||||
if id > len(db.data)-1 {
|
||||
return record, fmt.Errorf("no record config found for id %d", id)
|
||||
if int(id) > len(db.data)-1 {
|
||||
return record, fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
|
||||
}
|
||||
return db.data[id], nil
|
||||
}
|
||||
|
||||
func (db *database) SelectAll() (records []models.Record) {
|
||||
func (db *Database) SelectAll() (records []records.Record) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
return db.data
|
||||
|
||||
@@ -4,20 +4,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
return db.persistentDB.GetEvents(domain, host)
|
||||
}
|
||||
|
||||
func (db *database) Update(id int, record models.Record) error {
|
||||
func (db *Database) Update(id uint, record records.Record) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
if id < 0 {
|
||||
return fmt.Errorf("id %d cannot be lower than 0", id)
|
||||
}
|
||||
if id > len(db.data)-1 {
|
||||
return fmt.Errorf("no record config found for id %d", id)
|
||||
if int(id) > len(db.data)-1 {
|
||||
return fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
|
||||
}
|
||||
currentCount := len(db.data[id].History)
|
||||
newCount := len(record.History)
|
||||
@@ -25,8 +23,8 @@ func (db *database) Update(id int, record models.Record) error {
|
||||
// new IP address added
|
||||
if newCount > currentCount {
|
||||
if err := db.persistentDB.StoreNewIP(
|
||||
record.Settings.Domain,
|
||||
record.Settings.Host,
|
||||
record.Settings.Domain(),
|
||||
record.Settings.Host(),
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetSuccessTime(),
|
||||
); err != nil {
|
||||
@@ -35,3 +33,9 @@ func (db *database) Update(id int, record models.Record) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Close() (err error) {
|
||||
db.Lock() // ensure write operation finishes
|
||||
defer db.Unlock()
|
||||
return db.persistentDB.Close()
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/html"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
// Handler contains a handler function
|
||||
type Handler interface {
|
||||
GetHandlerFunc() http.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
rootURL string
|
||||
uiDir string
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
forceUpdate func()
|
||||
onError func(err error)
|
||||
getTime func() time.Time
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler object
|
||||
func NewHandler(rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate func(), onError func(err error)) Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
uiDir: uiDir,
|
||||
db: db,
|
||||
logger: logger,
|
||||
forceUpdate: forceUpdate,
|
||||
onError: onError,
|
||||
getTime: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandlerFunc returns a router with all the necessary routes configured
|
||||
func (h *handler) GetHandlerFunc() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("received HTTP request at %s", r.RequestURI)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
// TODO: Forms to change existing updates or add some
|
||||
t := template.Must(template.ParseFiles(h.uiDir + "/ui/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := html.ConvertRecord(record, h.getTime())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
h.logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
|
||||
h.logger.Info("Update started manually")
|
||||
h.forceUpdate()
|
||||
http.Redirect(w, r, h.rootURL, 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
57
internal/health/check.go
Normal file
57
internal/health/check.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
)
|
||||
|
||||
func MakeIsHealthy(db AllSelecter, resolver LookupIPer) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRecordUpdateFailed = errors.New("record update failed")
|
||||
ErrRecordIPNotSet = errors.New("record IP not set")
|
||||
ErrLookupMismatch = errors.New("lookup IP addresses do not match")
|
||||
)
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%w: %s", ErrRecordUpdateFailed, record.String())
|
||||
} else if record.Settings.Proxied() {
|
||||
continue
|
||||
}
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
lookedUpIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, hostname)
|
||||
}
|
||||
found := false
|
||||
lookedUpIPsString := make([]string, len(lookedUpIPs))
|
||||
for i, lookedUpIP := range lookedUpIPs {
|
||||
lookedUpIPsString[i] = lookedUpIP.String()
|
||||
if lookedUpIP.Equal(currentIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("%w: %s instead of %s for %s",
|
||||
ErrLookupMismatch, strings.Join(lookedUpIPsString, ","), currentIP, hostname)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
internal/health/client.go
Normal file
53
internal/health/client.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
const timeout = 5 * time.Second
|
||||
return &Client{
|
||||
Client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
var ErrUnhealthy = errors.New("program is unhealthy")
|
||||
|
||||
// Query sends an HTTP request to the other instance of
|
||||
// the program, and to its internal healthcheck server.
|
||||
func (c *Client) Query(ctx context.Context, port uint16) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+strconv.Itoa(int(port)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading body from response with status %s: %w", resp.Status, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", ErrUnhealthy, string(b))
|
||||
}
|
||||
28
internal/health/handler.go
Normal file
28
internal/health/handler.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func newHandler(healthcheck func() error) http.Handler {
|
||||
return &handler{
|
||||
healthcheck: healthcheck,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
healthcheck func() error
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || (r.RequestURI != "" && r.RequestURI != "/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err := h.healthcheck()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
22
internal/health/interfaces.go
Normal file
22
internal/health/interfaces.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type AllSelecter interface {
|
||||
SelectAll() (records []records.Record)
|
||||
}
|
||||
|
||||
type LookupIPer interface {
|
||||
LookupIP(ctx context.Context, network, host string) (ips []net.IP, err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
52
internal/health/server.go
Normal file
52
internal/health/server.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
address string
|
||||
logger Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewServer(address string, logger Logger, healthcheck func() error) *Server {
|
||||
handler := newHandler(healthcheck)
|
||||
return &Server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
server := http.Server{
|
||||
Addr: s.address,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
err := server.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
s.logger.Error("failed shutting down: " + err.Error())
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on " + s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err.Error())
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type lookupIPFunc func(host string) ([]net.IP, error)
|
||||
|
||||
// IsHealthy checks all the records were updated successfully and returns an error if not
|
||||
func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.Warn("unhealthy: %s", err)
|
||||
}
|
||||
}()
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.NoDNSLookup {
|
||||
continue
|
||||
}
|
||||
lookedUpIPs, err := lookupIP(record.Settings.BuildDomainName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no set IP address found")
|
||||
}
|
||||
for _, lookedUpIP := range lookedUpIPs {
|
||||
if !lookedUpIP.Equal(currentIP) {
|
||||
return fmt.Errorf(
|
||||
"lookup IP address of %s is %s instead of %s",
|
||||
record.Settings.BuildDomainName(), lookedUpIP, currentIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := models.HTMLRow{
|
||||
Domain: convertDomain(record.Settings.BuildDomainName()),
|
||||
Host: models.HTML(record.Settings.Host),
|
||||
Provider: convertProvider(record.Settings.Provider),
|
||||
IPMethod: convertIPMethod(record.Settings.IPMethod, record.Settings.Provider),
|
||||
}
|
||||
message := record.Message
|
||||
if record.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + record.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(record.Status) == 0 {
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(record.Status),
|
||||
message,
|
||||
time.Since(record.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := record.History.GetPreviousIPs()
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return constants.HTMLSuccess
|
||||
case constants.FAIL:
|
||||
return constants.HTMLFail
|
||||
case constants.UPTODATE:
|
||||
return constants.HTMLUpdate
|
||||
case constants.UPDATING:
|
||||
return constants.HTMLUpdating
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
|
||||
func convertProvider(provider models.Provider) models.HTML {
|
||||
switch provider {
|
||||
case constants.NAMECHEAP:
|
||||
return constants.HTMLNamecheap
|
||||
case constants.GODADDY:
|
||||
return constants.HTMLGodaddy
|
||||
case constants.DUCKDNS:
|
||||
return constants.HTMLDuckDNS
|
||||
case constants.DREAMHOST:
|
||||
return constants.HTMLDreamhost
|
||||
case constants.CLOUDFLARE:
|
||||
return constants.HTMLCloudflare
|
||||
case constants.NOIP:
|
||||
return constants.HTMLNoIP
|
||||
case constants.DNSPOD:
|
||||
return constants.HTMLDNSPod
|
||||
case constants.INFOMANIAK:
|
||||
return constants.HTMLInfomaniak
|
||||
case constants.DDNSSDE:
|
||||
return constants.HTMLDdnssde
|
||||
default:
|
||||
s := string(provider)
|
||||
if strings.HasPrefix("https://", s) {
|
||||
shorterName := strings.TrimPrefix(s, "https://")
|
||||
shorterName = strings.TrimSuffix(shorterName, "/")
|
||||
return models.HTML(fmt.Sprintf("<a href=\"%s\">%s</a>", s, shorterName))
|
||||
}
|
||||
return models.HTML(string(provider))
|
||||
}
|
||||
}
|
||||
|
||||
func convertIPMethod(ipMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
// TODO map to icons
|
||||
switch ipMethod {
|
||||
case constants.PROVIDER:
|
||||
return convertProvider(provider)
|
||||
case constants.OPENDNS:
|
||||
return constants.HTMLOpenDNS
|
||||
case constants.IFCONFIG:
|
||||
return constants.HTMLIfconfig
|
||||
case constants.IPINFO:
|
||||
return constants.HTMLIpinfo
|
||||
case constants.IPIFY:
|
||||
return constants.HTMLIpify
|
||||
case constants.IPIFY6:
|
||||
return constants.HTMLIpify6
|
||||
case constants.DDNSS:
|
||||
return constants.HTMLDdnss
|
||||
case constants.DDNSS4:
|
||||
return constants.HTMLDdnss4
|
||||
case constants.DDNSS6:
|
||||
return constants.HTMLDdnss6
|
||||
case constants.CYCLE:
|
||||
return constants.HTMLCycle
|
||||
default:
|
||||
return models.HTML(string(ipMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func convertDomain(domain string) models.HTML {
|
||||
return models.HTML("<a href=\"http://" + domain + "\">" + domain + "</a>")
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
// Provider is a possible DNS provider.
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
// Status is the record config status.
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
// HTML is for constants HTML strings.
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user