├── .commitlintrc.yml ├── .dockerignore ├── .github ├── scripts │ └── update-flake-version └── workflows │ ├── flow-pr-meta.yml │ ├── flow-pr.yml │ ├── flow-release.yml │ ├── job-lint.yml │ └── job-test.yml ├── .gitignore ├── .goreleaser.yml ├── .goreleaser.ytt.yml ├── .releaserc.yml ├── .yamllint.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── Taskfile.yaml ├── cmd ├── cloudflare.go ├── cmd.go └── ip.go ├── dev.Dockerfile ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── internal └── execext │ ├── devnull.go │ └── exec.go ├── lefthook.yml ├── main.go ├── package.nix ├── renovate.json5 ├── scripts └── update_readme ├── shell.nix ├── systemd ├── cloudflare-dynamic-dns@.service └── cloudflare-dynamic-dns@.timer ├── vendir.lock.yml └── vendir.yml /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "@commitlint/config-conventional" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !cmd/ 3 | !go.mod 4 | !go.sum 5 | !main.go 6 | -------------------------------------------------------------------------------- /.github/scripts/update-flake-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # If Github Action is running with debug enabled, print all commands 6 | # Otherwise, silence standard output 7 | if [[ "${RUNNER_DEBUG:-}" == "1" ]]; then 8 | set -x 9 | else 10 | exec 1>/dev/null 11 | fi 12 | 13 | # Replace version in flake.nix 14 | # If the pattern is not found, exit with code 128 15 | # If the version is not set, exit with code 1 16 | # shellcheck disable=SC2016 17 | sed -i -r '/(baseVersion = ")[^"]+(";)/,${s//\1'"${1:?Set version}"'\2/;b};$q128' package.nix 18 | 19 | # Run nix-update to update vendorHash 20 | time nix run github:Mic92/nix-update -- --flake cloudflare-dynamic-dns --version=skip 21 | -------------------------------------------------------------------------------- /.github/workflows/flow-pr-meta.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR Meta Flow 3 | 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - edited 9 | - synchronize 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | validate-title: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3 19 | id: lint_pr_title 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2 24 | # When the previous steps fails, the workflow would stop. By adding this 25 | # condition you can continue the execution with the populated error message. 26 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 27 | with: 28 | header: pr-title-lint-error 29 | message: | 30 | Hey there and thank you for opening this pull request! 👋🏼 31 | 32 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 33 | 34 | Details: 35 | 36 | ``` 37 | ${{ steps.lint_pr_title.outputs.error_message }} 38 | ``` 39 | 40 | # Delete a previous comment when the issue has been resolved 41 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 42 | uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2 43 | with: 44 | header: pr-title-lint-error 45 | delete: true 46 | -------------------------------------------------------------------------------- /.github/workflows/flow-pr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR Flow 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | paths: 9 | - 'main.go' 10 | - 'go.mod' 11 | - 'go.sum' 12 | - '.goreleaser.yml' 13 | - 'cmd/**' 14 | 15 | jobs: 16 | lint: 17 | uses: ./.github/workflows/job-lint.yml 18 | test: 19 | uses: ./.github/workflows/job-test.yml 20 | nix-update: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31.4.0 28 | with: 29 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Try to update nix flake 31 | run: ./.github/scripts/update-flake-version pr-${{ github.event.pull_request.number }} 32 | - name: Print changes 33 | run: git diff 34 | -------------------------------------------------------------------------------- /.github/workflows/flow-release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Flow 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint: 11 | uses: ./.github/workflows/job-lint.yml 12 | test: 13 | uses: ./.github/workflows/job-test.yml 14 | release: 15 | needs: 16 | - test 17 | - lint 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | packages: write 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | persist-credentials: false 27 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31.4.0 28 | with: 29 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Import GPG key 31 | uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 32 | with: 33 | gpg_private_key: ${{ secrets.ZEBRADIL_BOT_GPG_PRIVATE_KEY }} 34 | git_user_signingkey: true 35 | git_commit_gpgsign: true 36 | - name: Semantic Release 37 | id: semantic 38 | uses: cycjimmy/semantic-release-action@c4a2fa890676fc2db25ad0aacd8ab4a0f1f4c024 # v4.2.1 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.ZEBRADIL_BOT_GITHUB_TOKEN }} 41 | GIT_AUTHOR_NAME: Zebradli Bot 42 | GIT_AUTHOR_EMAIL: german.lashevich+github-zebradil-bot@gmail.com 43 | GIT_COMMITTER_NAME: Zebradli Bot 44 | GIT_COMMITTER_EMAIL: german.lashevich+github-zebradil-bot@gmail.com 45 | with: 46 | extra_plugins: | 47 | @semantic-release/exec 48 | @semantic-release/git 49 | - name: Print committed changes 50 | run: git show 51 | - name: Install GoReleaser 52 | if: steps.semantic.outputs.new_release_published == 'true' 53 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 54 | with: 55 | install-only: true 56 | - name: Setup QEMU 57 | if: steps.semantic.outputs.new_release_published == 'true' 58 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 59 | - name: Setup Docker Buildx 60 | if: steps.semantic.outputs.new_release_published == 'true' 61 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 62 | - name: Login to GHCR 63 | if: steps.semantic.outputs.new_release_published == 'true' 64 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 65 | with: 66 | registry: ghcr.io 67 | username: ${{ github.actor }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | - name: Publish release 70 | if: steps.semantic.outputs.new_release_published == 'true' 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }} 74 | run: | 75 | goreleaser release --clean \ 76 | --release-notes <(echo "${{ steps.semantic.outputs.new_release_notes }}") 77 | -------------------------------------------------------------------------------- /.github/workflows/job-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Job 3 | 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 16 | with: 17 | go-version-file: ./go.mod 18 | cache: false 19 | - name: Install gofumpt 20 | uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0 21 | with: 22 | repo: mvdan/gofumpt 23 | tag: v0.7.0 24 | cache: enable 25 | extension-matching: nah 26 | rename-to: gofumpt 27 | chmod: 0700 28 | - name: Check code formatting 29 | run: | 30 | set -euo pipefail 31 | gofumpt -l $( \ 32 | git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT \ 33 | "origin/$GITHUB_BASE_REF..origin/$GITHUB_HEAD_REF" \ 34 | | grep '\.go$' \ 35 | ) \ 36 | | tee /dev/stderr \ 37 | | test $(wc -l) -eq 0 38 | - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 39 | -------------------------------------------------------------------------------- /.github/workflows/job-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Job 3 | 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 16 | with: 17 | go-version-file: ./go.mod 18 | - name: Test 19 | run: go test -v ./... 20 | - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 21 | with: 22 | version: latest 23 | args: build --snapshot --clean 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: cloudflare-dynamic-dns 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - binary: cloudflare-dynamic-dns 8 | env: 9 | - CGO_ENABLED=0 10 | targets: 11 | - linux_amd64 12 | - linux_386 13 | - linux_arm64 14 | - linux_arm_6 15 | - linux_arm_7 16 | - linux_mips 17 | - linux_mipsle 18 | - linux_mips64 19 | - linux_mips64le 20 | - linux_riscv64 21 | - darwin_amd64 22 | - darwin_arm64 23 | - windows_amd64 24 | - windows_386 25 | archives: 26 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 27 | format_overrides: 28 | - goos: windows 29 | format: zip 30 | files: 31 | - LICENSE 32 | - README.md 33 | - systemd/* 34 | checksum: 35 | name_template: checksums.txt 36 | snapshot: 37 | name_template: '{{ incpatch .Version }}-next' 38 | aurs: 39 | - name: cloudflare-dynamic-dns-bin 40 | homepage: https://github.com/zebradil/cloudflare-dynamic-dns 41 | description: Dynamic DNS client for Cloudflare with IPv6/IPv4 support 42 | maintainers: 43 | - German Lashevich 44 | license: MIT 45 | private_key: '{{ .Env.AUR_SSH_KEY }}' 46 | git_url: ssh://aur@aur.archlinux.org/cloudflare-dynamic-dns-bin.git 47 | package: |- 48 | BIN=cloudflare-dynamic-dns 49 | 50 | install -Dm755 ./$BIN -t "${pkgdir}/usr/bin" 51 | install -Dm644 systemd/* -t "$pkgdir"/usr/lib/systemd/system 52 | install -m700 -d "$pkgdir"/etc/cloudflare-dynamic-dns/config.d 53 | 54 | # completions 55 | mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" 56 | mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" 57 | mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" 58 | ./$BIN completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/$BIN" 59 | ./$BIN completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/$BIN.fish" 60 | ./$BIN completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_$BIN" 61 | commit_author: 62 | name: Zebradil Bot 63 | email: german.lashevich+github-zebradil-bot@gmail.com 64 | nfpms: 65 | - vendor: Zebradil 66 | homepage: https://github.com/zebradil/cloudflare-dynamic-dns 67 | maintainer: German Lashevich 68 | description: Dynamic DNS client for Cloudflare with IPv6/IPv4 support 69 | license: MIT 70 | formats: 71 | - apk 72 | - deb 73 | - rpm 74 | contents: 75 | - src: systemd/ 76 | dst: /usr/lib/systemd/system/ 77 | dockers: 78 | - goos: linux 79 | goarch: amd64 80 | image_templates: 81 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-amd64 82 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-amd64 83 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-amd64 84 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-amd64 85 | use: buildx 86 | build_flag_templates: 87 | - --platform=linux/amd64 88 | - goos: linux 89 | goarch: arm64 90 | image_templates: 91 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm64 92 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm64 93 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm64 94 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm64 95 | use: buildx 96 | build_flag_templates: 97 | - --platform=linux/arm64 98 | - goos: linux 99 | goarch: arm 100 | goarm: "6" 101 | image_templates: 102 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm-6 103 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm-6 104 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm-6 105 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm-6 106 | use: buildx 107 | build_flag_templates: 108 | - --platform=linux/arm/6 109 | - goos: linux 110 | goarch: arm 111 | goarm: "7" 112 | image_templates: 113 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm-7 114 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm-7 115 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm-7 116 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm-7 117 | use: buildx 118 | build_flag_templates: 119 | - --platform=linux/arm/7 120 | - goos: linux 121 | goarch: riscv64 122 | image_templates: 123 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-riscv64 124 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-riscv64 125 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-riscv64 126 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-riscv64 127 | use: buildx 128 | build_flag_templates: 129 | - --platform=linux/riscv64 130 | docker_manifests: 131 | - name_template: ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }} 132 | image_templates: 133 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-amd64 134 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm64 135 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm-6 136 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-arm-7 137 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Tag }}-linux-riscv64 138 | - name_template: ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }} 139 | image_templates: 140 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-amd64 141 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm64 142 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm-6 143 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-arm-7 144 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}-linux-riscv64 145 | - name_template: ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }} 146 | image_templates: 147 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-amd64 148 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm64 149 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm-6 150 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-arm-7 151 | - ghcr.io/zebradil/cloudflare-dynamic-dns:{{ .Major }}.{{ .Minor }}-linux-riscv64 152 | - name_template: ghcr.io/zebradil/cloudflare-dynamic-dns:latest 153 | image_templates: 154 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-amd64 155 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm64 156 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm-6 157 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-arm-7 158 | - ghcr.io/zebradil/cloudflare-dynamic-dns:latest-linux-riscv64 159 | -------------------------------------------------------------------------------- /.goreleaser.ytt.yml: -------------------------------------------------------------------------------- 1 | #@ project_name = 'cloudflare-dynamic-dns' 2 | #@ description = 'Dynamic DNS client for Cloudflare with IPv6/IPv4 support' 3 | #@ maintainer = 'German Lashevich ' 4 | #@ url = 'https://github.com/zebradil/cloudflare-dynamic-dns' 5 | #@ committer_name = 'Zebradil Bot' 6 | #@ committer_email = 'german.lashevich+github-zebradil-bot@gmail.com' 7 | #@ targets = [ 8 | #@ ("linux", "amd64", ""), 9 | #@ ("linux", "386", ""), 10 | #@ ("linux", "arm64", ""), 11 | #@ ("linux", "arm", "6"), 12 | #@ ("linux", "arm", "7"), 13 | #@ ("linux", "mips", ""), 14 | #@ ("linux", "mipsle", ""), 15 | #@ ("linux", "mips64", ""), 16 | #@ ("linux", "mips64le", ""), 17 | #@ ("linux", "riscv64", ""), 18 | #@ ("darwin", "amd64", ""), 19 | #@ ("darwin", "arm64", ""), 20 | #@ ("windows", "amd64", ""), 21 | #@ ("windows", "386", ""), 22 | #@ ] 23 | #@ docker_targets = [ 24 | #@ ("linux", "amd64", ""), 25 | #@ ("linux", "arm64", ""), 26 | #@ ("linux", "arm", "6"), 27 | #@ ("linux", "arm", "7"), 28 | #@ ("linux", "riscv64", ""), 29 | #@ ] 30 | #@ versions = [ 31 | #@ '{{ .Tag }}', 32 | #@ '{{ .Major }}', 33 | #@ '{{ .Major }}.{{ .Minor }}', 34 | #@ 'latest', 35 | #@ ] 36 | 37 | #@ base_image = 'ghcr.io/zebradil/' + project_name 38 | 39 | #@ def make_target(os, arch, arm): 40 | #@ target = os + "_" + arch 41 | #@ if arm: 42 | #@ target += "_" + arm 43 | #@ end 44 | #@ return target 45 | #@ end 46 | 47 | #@ def make_image(version, os, arch, arm): 48 | #@ image = base_image + ":" + version + "-" + os + "-" + arch 49 | #@ if arm: 50 | #@ image += "-" + arm 51 | #@ end 52 | #@ return image 53 | #@ end 54 | 55 | #@ def make_platform(os, arch, arm): 56 | #@ platform = "--platform=" + os + "/" + arch 57 | #@ if arm: 58 | #@ platform += "/" + arm 59 | #@ end 60 | #@ return platform 61 | #@ end 62 | 63 | --- 64 | version: 2 65 | project_name: #@ project_name 66 | before: 67 | hooks: 68 | - go mod tidy 69 | builds: 70 | - binary: #@ project_name 71 | env: 72 | - CGO_ENABLED=0 73 | targets: 74 | #@ for/end os, arch, arm in targets: 75 | - #@ make_target(os, arch, arm) 76 | archives: 77 | - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 78 | format_overrides: 79 | - goos: windows 80 | format: zip 81 | files: 82 | - LICENSE 83 | - README.md 84 | - systemd/* 85 | checksum: 86 | name_template: "checksums.txt" 87 | snapshot: 88 | name_template: "{{ incpatch .Version }}-next" 89 | aurs: 90 | - name: #@ project_name + "-bin" 91 | homepage: #@ url 92 | description: #@ description 93 | maintainers: 94 | - #@ maintainer 95 | license: "MIT" 96 | private_key: "{{ .Env.AUR_SSH_KEY }}" 97 | git_url: #@ "ssh://aur@aur.archlinux.org/{}-bin.git".format(project_name) 98 | #@yaml/text-templated-strings 99 | package: |- 100 | BIN=(@= project_name @) 101 | 102 | install -Dm755 ./$BIN -t "${pkgdir}/usr/bin" 103 | install -Dm644 systemd/* -t "$pkgdir"/usr/lib/systemd/system 104 | install -m700 -d "$pkgdir"/etc/cloudflare-dynamic-dns/config.d 105 | 106 | # completions 107 | mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" 108 | mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" 109 | mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" 110 | ./$BIN completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/$BIN" 111 | ./$BIN completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/$BIN.fish" 112 | ./$BIN completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_$BIN" 113 | commit_author: 114 | name: #@ committer_name 115 | email: #@ committer_email 116 | nfpms: 117 | - vendor: Zebradil 118 | homepage: #@ url 119 | maintainer: #@ maintainer 120 | description: #@ description 121 | license: MIT 122 | formats: 123 | - apk 124 | - deb 125 | - rpm 126 | contents: 127 | - src: systemd/ 128 | dst: /usr/lib/systemd/system/ 129 | dockers: 130 | #@ for/end os, arch, arm in docker_targets: 131 | - goos: #@ os 132 | goarch: #@ arch 133 | #@ if/end arm: 134 | goarm: #@ arm 135 | image_templates: 136 | #@ for/end version in versions: 137 | - #@ make_image(version, os, arch, arm) 138 | use: buildx 139 | build_flag_templates: 140 | - #@ make_platform(os, arch, arm) 141 | docker_manifests: 142 | #@ for/end version in versions: 143 | - name_template: #@ base_image + ":" + version 144 | image_templates: 145 | #@ for/end os, arch, arm in docker_targets: 146 | - #@ make_image(version, os, arch, arm) 147 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | branches: 3 | - main 4 | plugins: 5 | - '@semantic-release/commit-analyzer' 6 | - - '@semantic-release/exec' 7 | - prepareCmd: .github/scripts/update-flake-version ${nextRelease.version} 8 | - '@semantic-release/release-notes-generator' 9 | - '@semantic-release/changelog' 10 | - - '@semantic-release/git' 11 | - assets: 12 | - CHANGELOG.md 13 | - package.nix 14 | tagFormat: '${version}' 15 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | comments: 6 | min-spaces-from-content: 1 7 | require-starting-space: false 8 | line-length: 9 | max: 120 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.3.19](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.18...4.3.19) (2025-03-26) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update module github.com/spf13/viper to v1.20.1 ([#210](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/210)) ([1cd6742](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/1cd67425f4bec423fddb2515c77650519cefc3f9)) 7 | 8 | ## [4.3.18](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.17...4.3.18) (2025-03-17) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update module github.com/spf13/viper to v1.20.0 ([#204](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/204)) ([3999a0c](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/3999a0c0d9173eb20aafaee8875ffd9fa8490af8)) 14 | * **deps:** update module mvdan.cc/sh/v3 to v3.11.0 ([#199](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/199)) ([fae0818](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fae08180bc8e4c7aa9545a6aac5288dea28ab258)) 15 | 16 | ## [4.3.17](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.16...4.3.17) (2025-02-26) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * base container image off Alpine with curl ([#194](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/194)) ([04faeac](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/04faeac10f8a07109f0d4eacd432b1b96a28bfd6)), closes [#191](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/191) 22 | 23 | ## [4.3.16](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.15...4.3.16) (2025-02-26) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * **deps:** update module github.com/spf13/cobra to v1.9.0 ([#189](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/189)) ([7777452](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/7777452d93667220a00d9e609b27aa20950cfecc)) 29 | * **deps:** update module github.com/spf13/cobra to v1.9.1 ([#190](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/190)) ([c75a392](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/c75a392883716716e2fb3387190a3bb48d309ca5)) 30 | 31 | ## [4.3.15](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.14...4.3.15) (2025-01-30) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.115.0 ([#176](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/176)) ([2c8d232](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/2c8d23226d18e98c0a1833384a2d2effa01e762a)) 37 | 38 | ## [4.3.14](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.13...4.3.14) (2025-01-15) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.114.0 ([#171](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/171)) ([f9ccdea](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/f9ccdeabc04919fcf3bcfd538360a082fc55d7ba)) 44 | 45 | ## [4.3.13](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.12...4.3.13) (2025-01-02) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.113.0 ([#167](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/167)) ([e10eb99](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/e10eb99b6622165492610616eafad77d805644cd)) 51 | 52 | ## [4.3.12](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.11...4.3.12) (2024-12-18) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.112.0 ([#166](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/166)) ([a8780cc](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/a8780cc0ab22c64b0c9e8d638b4b6b4d62250e56)) 58 | 59 | ## [4.3.11](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.10...4.3.11) (2024-12-04) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.111.0 ([#162](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/162)) ([c767d3b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/c767d3be55c633aece9da6866c76c1d3ddc30cd3)) 65 | 66 | ## [4.3.10](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.9...4.3.10) (2024-11-20) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.110.0 ([#159](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/159)) ([2c5aaab](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/2c5aaabf0ed1f32411259b79a744be98aa332991)) 72 | 73 | ## [4.3.9](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.8...4.3.9) (2024-11-06) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.109.0 ([ea49653](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/ea49653665a24620926ca1364e0e82212659e349)) 79 | 80 | ## [4.3.8](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.7...4.3.8) (2024-10-23) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.108.0 ([446def8](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/446def833f5c8233a99f55e024d12c47996b9fa5)) 86 | 87 | ## [4.3.7](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.6...4.3.7) (2024-10-20) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **deps:** update module mvdan.cc/sh/v3 to v3.10.0 ([9966c53](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/9966c5303b8b4aa437c1159b64f5c513aa0cbd5c)) 93 | 94 | ## [4.3.6](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.5...4.3.6) (2024-10-09) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.107.0 ([5f51b60](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/5f51b609879330e079685aa2e669555ea5848850)) 100 | 101 | ## [4.3.5](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.4...4.3.5) (2024-09-27) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.106.0 ([150fab9](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/150fab96e83eb17f906903ff94b214c363a868f9)) 107 | 108 | ## [4.3.4](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.3...4.3.4) (2024-09-25) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.105.0 ([0d91318](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/0d91318fb4216fe0f00b796bb8ded5ba79c2b4b0)) 114 | 115 | ## [4.3.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.2...4.3.3) (2024-09-11) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.104.0 ([84bcfa3](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/84bcfa36e9a280d776c892d4a3dbaae61c7ff229)) 121 | 122 | ## [4.3.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.1...4.3.2) (2024-08-28) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * **release:** rename commited file flake -> package ([ac4cb0b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/ac4cb0b0f459e127d2416e9ce6f81734964ad5be)) 128 | 129 | ## [4.3.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.3.0...4.3.1) (2024-08-28) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.103.0 ([d8595b1](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/d8595b110d8afbdaf9a71d373333f2cc5e97d0df)) 135 | 136 | # [4.3.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.15...4.3.0) (2024-08-23) 137 | 138 | 139 | ### Features 140 | 141 | * add option to get IP from command ([#132](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/132)) ([4d1c910](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/4d1c910d9ac8d49d8ea684bd9dc87af2259caf8d)) 142 | 143 | ## [4.2.15](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.14...4.2.15) (2024-08-18) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * correct IP selection logic ([#127](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/127)) ([8f7c959](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/8f7c959b8a1e10a7506af23e56b0e52f34ed529c)) 149 | 150 | ## [4.2.14](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.13...4.2.14) (2024-08-14) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.102.0 ([6dfd715](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/6dfd715e60f566702875c15af7284c1ba64a9bca)) 156 | 157 | ## [4.2.13](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.12...4.2.13) (2024-07-31) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.101.0 ([7df7ba9](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/7df7ba9e593fe12bd814350957ee99a4ce5d5fb7)) 163 | 164 | ## [4.2.12](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.11...4.2.12) (2024-07-25) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * use publicsuffix for DNS zone detection ([bcf72ed](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/bcf72ed503c00809385c8e75af0014d2c79081da)), closes [#125](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/125) 170 | 171 | ## [4.2.11](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.10...4.2.11) (2024-07-18) 172 | 173 | 174 | ### Bug Fixes 175 | 176 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.100.0 ([6321851](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/6321851bd2b4ed2aae645ac1c05ad79d6eb3cc38)) 177 | 178 | ## [4.2.10](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.9...4.2.10) (2024-07-03) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.99.0 ([d4a3292](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/d4a3292dd845bffd6ea84ae6ca492b400edff31b)) 184 | 185 | ## [4.2.9](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.8...4.2.9) (2024-06-19) 186 | 187 | 188 | ### Bug Fixes 189 | 190 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.98.0 ([30fa753](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/30fa7537b7b6dfe1c39a18cbe7f2df0983a25ec0)) 191 | 192 | ## [4.2.8](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.7...4.2.8) (2024-06-14) 193 | 194 | 195 | ### Bug Fixes 196 | 197 | * **deps:** update module github.com/spf13/cobra to v1.8.1 ([917d97d](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/917d97ddc4311aecaf64a05e0972ffb2ca48d4e8)) 198 | 199 | ## [4.2.7](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.6...4.2.7) (2024-06-05) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.97.0 ([0a034b2](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/0a034b23dc17745a8396f5deb2f6824693e6dd9b)) 205 | 206 | ## [4.2.6](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.5...4.2.6) (2024-06-02) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * **deps:** update module github.com/spf13/viper to v1.19.0 ([608547d](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/608547d20198c500704008a467053f5c9413aabd)) 212 | 213 | ## [4.2.5](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.4...4.2.5) (2024-05-22) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.96.0 ([cdefa33](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/cdefa333fa68474e7a51460d66835b53b1ea0669)) 219 | 220 | ## [4.2.4](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.3...4.2.4) (2024-05-08) 221 | 222 | 223 | ### Bug Fixes 224 | 225 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.95.0 ([dbb66bd](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/dbb66bda4d10c1018cb4470b0554b9f60a8e614a)) 226 | 227 | ## [4.2.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.2...4.2.3) (2024-04-24) 228 | 229 | 230 | ### Bug Fixes 231 | 232 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.94.0 ([fa69e96](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fa69e969b3930adf8eeff8c8dc6927364e9428bd)) 233 | 234 | ## [4.2.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.1...4.2.2) (2024-04-10) 235 | 236 | 237 | ### Bug Fixes 238 | 239 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.93.0 ([945a728](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/945a7288562994457cf1cb5bfc8b4213556aa235)) 240 | 241 | ## [4.2.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.2.0...4.2.1) (2024-04-07) 242 | 243 | 244 | ### Bug Fixes 245 | 246 | * correct usage of hashing function to output fixed length ([21d244a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/21d244a68cc51c50424a3f7e2b46bf31f1045102)) 247 | 248 | # [4.2.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.1.1...4.2.0) (2024-03-30) 249 | 250 | 251 | ### Features 252 | 253 | * support for ipv4 stack ([#106](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/106)) ([a70d619](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/a70d619aaced65e5db12fdef81c97ac81d8dd6c6)) 254 | 255 | ## [4.1.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.1.0...4.1.1) (2024-03-27) 256 | 257 | 258 | ### Bug Fixes 259 | 260 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.92.0 ([9f6b8e4](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/9f6b8e4786656cbf9ae8a057bfcb613bf6bfb383)) 261 | 262 | # [4.1.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.7...4.1.0) (2024-03-24) 263 | 264 | 265 | ### Features 266 | 267 | * add option to configure if a DNS record should be proxied ([b5836e2](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/b5836e2d7b78803850de31d5faab231de4542fe3)), closes [#101](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/101) 268 | 269 | ## [4.0.7](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.6...4.0.7) (2024-03-24) 270 | 271 | 272 | ### Bug Fixes 273 | 274 | * update records with empty records when multihost is disabled ([4ea5e38](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/4ea5e38195b338043ce290038cd1247566a73502)) 275 | 276 | ## [4.0.6](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.5...4.0.6) (2024-03-22) 277 | 278 | 279 | ### Bug Fixes 280 | 281 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.91.0 ([2924d3d](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/2924d3de9f651defa8f99996bb9a33c512819301)) 282 | 283 | ## [4.0.5](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.4...4.0.5) (2024-03-13) 284 | 285 | 286 | ### Bug Fixes 287 | 288 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.90.0 ([48ecd01](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/48ecd01d7431dc8d4390e70c806acf468a8891ae)) 289 | 290 | ## [4.0.4](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.3...4.0.4) (2024-03-02) 291 | 292 | 293 | ### Bug Fixes 294 | 295 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.89.0 ([400cfed](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/400cfed76b86d25323779d2565e50e56fb17303e)) 296 | 297 | ## [4.0.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.2...4.0.3) (2024-02-14) 298 | 299 | 300 | ### Bug Fixes 301 | 302 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.88.0 ([f96afbf](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/f96afbf2a942ea5618207787433b3a3bd62bd075)) 303 | 304 | ## [4.0.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.1...4.0.2) (2024-01-31) 305 | 306 | 307 | ### Bug Fixes 308 | 309 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.87.0 ([66a8fd8](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/66a8fd83e2affa94d40be5f1b98614628e72f057)) 310 | 311 | ## [4.0.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/4.0.0...4.0.1) (2024-01-17) 312 | 313 | 314 | ### Bug Fixes 315 | 316 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.86.0 ([0fce146](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/0fce14601be25394a43b48bf3cfeeef15f2108a5)) 317 | 318 | # [4.0.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/3.1.0...4.0.0) (2024-01-08) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * consistently use kebab-case for config keys ([429d6a9](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/429d6a972a95d9f27e527a1c920e14dfb63e0e6c)) 324 | 325 | 326 | ### BREAKING CHANGES 327 | 328 | * the `prioritySubnets` config key has been renamed to 329 | `priority-subnets` to be consistent with the other config keys. 330 | 331 | # [3.1.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/3.0.0...3.1.0) (2024-01-07) 332 | 333 | 334 | ### Features 335 | 336 | * rename with-state-file flag to state-file, fix systemd unit ([27803f9](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/27803f90cfb184580b28b6805c0995139f51146c)) 337 | 338 | # [3.0.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.8.1...3.0.0) (2024-01-07) 339 | 340 | 341 | ### Bug Fixes 342 | 343 | * add check for STATE_DIRECTORY when running in systemd mode ([25ef6de](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/25ef6de6c4e59a1e1793382e10945cdca1239626)) 344 | * copy only binary in the container image ([d82e524](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/d82e524fe91adb7161bec60b702a3ea7857e4a62)) 345 | * do not shadow variable via assignment ([19c7d5a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/19c7d5a577d518f47e66375eed5b89ed56ca9c45)) 346 | * switching between single- and multi-host modes ([70e39af](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/70e39af2290466ec3747a84a85bb3a46bb0bdf21)) 347 | * validate token and iface values ([3ab4e44](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/3ab4e4488da1a479f71fc9d1baf3052201bf75b5)) 348 | 349 | 350 | ### Features 351 | 352 | * add --run-every to run periodically ([ac2af3c](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/ac2af3c7f558b267ef06bc0d1648f6996fa87f60)) 353 | * include "managed by" in the DNS record comment ([d829f28](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/d829f282770d9cd1b8fc1ff9cb39b79230f359f3)) 354 | * replace --systemd flag with --with-state-file ([adbc8df](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/adbc8df70ee216d89fcfa035a7d8b20a8cb979a6)) 355 | 356 | 357 | ### BREAKING CHANGES 358 | 359 | * --systemd flag is replaced with --with-state-file. 360 | 361 | ## [2.8.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.8.0...2.8.1) (2024-01-07) 362 | 363 | 364 | ### Bug Fixes 365 | 366 | * set path to config file via envvar ([4623ac7](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/4623ac74a303f84c2e133d84ec25d26355f77e81)) 367 | 368 | # [2.8.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.7.0...2.8.0) (2024-01-07) 369 | 370 | 371 | ### Features 372 | 373 | * accept environment variables for configuration ([249d9d8](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/249d9d88aa105c3e6fc15b801c212be39c883985)) 374 | 375 | # [2.7.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.6.1...2.7.0) (2024-01-07) 376 | 377 | 378 | ### Features 379 | 380 | * add experimental multihost mode ([c5bcb0a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/c5bcb0a45ab9797140863c379080afbfd898ea9c)) 381 | 382 | ## [2.6.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.6.0...2.6.1) (2024-01-06) 383 | 384 | 385 | ### Bug Fixes 386 | 387 | * improve EUI-64 detection by checking for the FFFE injection ([fd4da5b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fd4da5b0e98005c54d62fc847d543babc7ff68a8)) 388 | * use 7th bit from the _left_ to identify EUI-64 addresses ([a144a4c](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/a144a4c2dfc7cc25d86bb9d11ae0a8d8b69d2cec)) 389 | 390 | # [2.6.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.5.1...2.6.0) (2024-01-05) 391 | 392 | 393 | ### Features 394 | 395 | * add --version flag and log ([8fe1e51](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/8fe1e515ab8d8d0f6ea6ff8f023ff519eced6713)) 396 | 397 | ## [2.5.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.5.0...2.5.1) (2024-01-03) 398 | 399 | 400 | ### Bug Fixes 401 | 402 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.85.0 ([24f1bb7](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/24f1bb7cec073b5279f383ccad491eda64645197)) 403 | 404 | # [2.5.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.4.0...2.5.0) (2024-01-01) 405 | 406 | 407 | ### Bug Fixes 408 | 409 | * log a warning if the selected IPv6 address is not optimal ([0a13ac3](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/0a13ac3f59f099cf9788cb3c544e6d583500a0f9)) 410 | 411 | 412 | ### Features 413 | 414 | * prefer EUI-64 over randomly generated identifiers ([3d5d772](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/3d5d772d3a57c2984a87bf9b4b9b25b40a31df30)) 415 | 416 | # [2.4.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.9...2.4.0) (2024-01-01) 417 | 418 | 419 | ### Features 420 | 421 | * prefer GUA over ULA when selecting IPv6 address ([fa8e21a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fa8e21aa1d41038a3f8f72da3145a8dcc33f2ce9)) 422 | 423 | ## [2.3.9](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.8...2.3.9) (2024-01-01) 424 | 425 | 426 | ### Bug Fixes 427 | 428 | * add ca-certificates to scratch image ([9579a11](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/9579a116874e922921f6137d1be916802e73d4fa)), closes [#90](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/90) 429 | 430 | ## [2.3.8](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.7...2.3.8) (2023-12-20) 431 | 432 | 433 | ### Bug Fixes 434 | 435 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.84.0 ([3cc6ae6](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/3cc6ae653ce8ddc42355a002826b10f6a8f4b508)) 436 | 437 | ## [2.3.7](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.6...2.3.7) (2023-12-18) 438 | 439 | 440 | ### Bug Fixes 441 | 442 | * **deps:** update module github.com/spf13/viper to v1.18.2 ([fb3084b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fb3084b8a43a2ad1728f2b927c167217115a2309)) 443 | 444 | ## [2.3.6](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.5...2.3.6) (2023-12-08) 445 | 446 | 447 | ### Bug Fixes 448 | 449 | * **deps:** update module github.com/spf13/viper to v1.18.1 ([4f85b3b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/4f85b3bcef1400b633a6ba9509746cab41622178)) 450 | 451 | ## [2.3.5](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.4...2.3.5) (2023-12-06) 452 | 453 | 454 | ### Bug Fixes 455 | 456 | * **deps:** update module github.com/spf13/viper to v1.18.0 ([f18c747](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/f18c747e8c38e878c50ee3e001b63dfce6baaa69)) 457 | 458 | ## [2.3.4](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.3...2.3.4) (2023-12-06) 459 | 460 | 461 | ### Bug Fixes 462 | 463 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.83.0 ([1700071](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/1700071de0c8ff37510f30d3677d5d87236656c6)) 464 | 465 | ## [2.3.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.2...2.3.3) (2023-11-22) 466 | 467 | 468 | ### Bug Fixes 469 | 470 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.82.0 ([9a3ad02](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/9a3ad023b12b02b609398dc130df4c1c2487c059)) 471 | 472 | ## [2.3.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.1...2.3.2) (2023-11-08) 473 | 474 | 475 | ### Bug Fixes 476 | 477 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.81.0 ([fe49f28](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fe49f28c52cca21fe9afdbdfbeea8fe8a83d2983)) 478 | 479 | ## [2.3.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.3.0...2.3.1) (2023-11-06) 480 | 481 | 482 | ### Bug Fixes 483 | 484 | * **ci:** adjust goreleaser config for less docker arches ([bbcfd76](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/bbcfd76585fbf3f67a463bb03b0f6fe056bfe8df)) 485 | 486 | # [2.3.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.8...2.3.0) (2023-11-06) 487 | 488 | 489 | ### Bug Fixes 490 | 491 | * **ci:** log in to ghcr.io ([3cb4299](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/3cb42996145e473e82cf14352f5a214cd995a7a0)) 492 | * **release:** install systemd units and create config dir in AUR package ([64d90df](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/64d90df804e52f743ce5e0b5d1c0a4a1ce3e5078)) 493 | * **release:** reduce docker build targets ([272f7df](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/272f7df3cfe218d3d7f0806c56a885e48949dae9)) 494 | * **release:** render goreleaser config ([b88e551](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/b88e55169f685061f515b732a684253f9df38029)) 495 | 496 | 497 | ### Features 498 | 499 | * **release:** add binary AUR package ([fc765be](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/fc765bece8f07c214e9ebbdbc9cc9a89ea3e80c0)) 500 | * **release:** add nfpms packages configuration ([a05c01b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/a05c01bc7e4a74f2bed076bc40f4a37d9540f64e)) 501 | 502 | ## [2.2.8](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.7...2.2.8) (2023-11-04) 503 | 504 | 505 | ### Bug Fixes 506 | 507 | * **deps:** update module github.com/spf13/cobra to v1.8.0 ([48517b6](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/48517b6033b109b4095d70fb119214dc2ac1d63c)) 508 | 509 | ## [2.2.7](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.6...2.2.7) (2023-10-25) 510 | 511 | 512 | ### Bug Fixes 513 | 514 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.80.0 ([d8b2975](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/d8b29753f280cf9c108c9d259bdd201234380df8)) 515 | 516 | ## [2.2.6](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.5...2.2.6) (2023-10-11) 517 | 518 | 519 | ### Bug Fixes 520 | 521 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.79.0 ([6ec4248](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/6ec42488dd4152eaeafc8b95c8e80aa3f8bb7a7f)) 522 | 523 | ## [2.2.5](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.4...2.2.5) (2023-10-06) 524 | 525 | 526 | ### Bug Fixes 527 | 528 | * **deps:** update module github.com/spf13/viper to v1.17.0 ([ca0d63c](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/ca0d63c3d8df248f761bf20c454d089b83cac6d0)) 529 | 530 | ## [2.2.4](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.3...2.2.4) (2023-09-27) 531 | 532 | 533 | ### Bug Fixes 534 | 535 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.78.0 ([bca9b6b](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/bca9b6bfd99803f579efef4b0c35e93d38efc39c)) 536 | 537 | ## [2.2.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.2...2.2.3) (2023-09-13) 538 | 539 | 540 | ### Bug Fixes 541 | 542 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.77.0 ([08b3e46](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/08b3e4606915f22db458eef8192b6e4a389d6569)) 543 | 544 | ## [2.2.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.1...2.2.2) (2023-08-30) 545 | 546 | 547 | ### Bug Fixes 548 | 549 | * **ci:** configure more platforms for Docker ([e27660f](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/e27660f1ad97bca0879e825a2e7aa9062b85b591)) 550 | 551 | ## [2.2.1](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.2.0...2.2.1) (2023-08-30) 552 | 553 | 554 | ### Bug Fixes 555 | 556 | * **ci:** trigger release ([5f74e40](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/5f74e40747c7bcdde73da1f9ad365aa219894397)) 557 | 558 | # [2.2.0](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.1.3...2.2.0) (2023-08-30) 559 | 560 | 561 | ### Features 562 | 563 | * add Docker support ([#64](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/64)) ([f67188a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/f67188ae08fccbc6b02eda31bc975b3b0ed76efa)) 564 | 565 | ## [2.1.3](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.1.2...2.1.3) (2023-08-30) 566 | 567 | 568 | ### Bug Fixes 569 | 570 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.76.0 ([a260354](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/a260354b4af4998d94fe3a44094bffb4fe71cc9c)) 571 | 572 | ## [2.1.2](https://github.com/Zebradil/cloudflare-dynamic-dns/compare/2.1.1...2.1.2) (2023-08-23) 573 | 574 | 575 | ### Bug Fixes 576 | 577 | * check returned error ([613a78f](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/613a78fde6d0e4f0995de5d9274f916dcddc9c51)) 578 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.65.0 ([1823074](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/182307434f89e6a81d7240fd8df5d7707ddf972f)) 579 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.66.0 ([409738a](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/409738a5846202c6a53cd7ef5bfe50846a6d66f5)) 580 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.68.0 ([89d3496](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/89d3496999abe68f1c5791060746b49fa20e8a74)) 581 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.69.0 ([#55](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/55)) ([07bd6aa](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/07bd6aae333b22ad2ee5a45117361891674d5bee)) 582 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.70.0 ([#56](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/56)) ([039fe31](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/039fe31514d54dc1571ac461fb6a9982141e2424)) 583 | * **deps:** update module github.com/cloudflare/cloudflare-go to v0.75.0 ([#57](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/57)) ([0acef0e](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/0acef0ea1b1c7a82d923751759169800b7fd6f96)) 584 | * **deps:** update module github.com/sirupsen/logrus to v1.9.2 ([28110bb](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/28110bb659b2b768c4b99373f459b5bc44d88e4a)) 585 | * **deps:** update module github.com/sirupsen/logrus to v1.9.3 ([#54](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/54)) ([bd1d917](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/bd1d91706f7e0f731ab8e272f32b8ccabae77f4d)) 586 | * **deps:** update module github.com/spf13/viper to v1.16.0 ([#53](https://github.com/Zebradil/cloudflare-dynamic-dns/issues/53)) ([bd77422](https://github.com/Zebradil/cloudflare-dynamic-dns/commit/bd77422b3d3a672af6094c0b52cfdb69ce8d7e80)) 587 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 2 | RUN apk add --no-cache curl=8.12.1-r0 3 | COPY cloudflare-dynamic-dns / 4 | ENTRYPOINT ["/cloudflare-dynamic-dns"] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2023 German Lashevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic DNS client for Cloudflare 2 | 3 | A CLI tool for updating A/AAAA record at Cloudflare DNS with the currently detected address of the specified network interface. 4 | 5 | ## Features 6 | 7 | - Supports: 8 | - IPv4 and IPv6 9 | - Multiple domains with the same address 10 | - Multiple hosts in the same domain 11 | - Tries to be smart about selecting the address to use 12 | - Includes systemd service and timer files for automation 13 | - Can be run in a Docker container 14 | - Configuration via command line arguments, config file or environment variables 15 | 16 | ## Usage 17 | 18 | The rest of this section is the output of `cloudflare-dynamic-dns --help`. 19 | 20 | 21 |
 22 | 
 23 | Selects an address from the specified network interface or via an external
 24 | command and updates A or AAAA records at Cloudflare for the configured domains.
 25 | Supports both IPv4 and IPv6.
 26 | 
 27 | Required configuration options
 28 | --------------------------------------------------------------------------------
 29 | 
 30 | --iface:   network interface name to look up for an address
 31 |   or
 32 | --ipcmd:   shell command to run to get the address, should return one address
 33 |            per line. Uses https://github.com/mvdan/sh as the shell.
 34 |            Examples:
 35 |              - curl -fsSL https://api6.ipify.org
 36 |              - echo -e "127.0.0.1\n127.0.0.2"
 37 | 
 38 | --domains: one or more domain names to assign the address to
 39 | --token:   Cloudflare API token with edit access rights to the DNS zone
 40 | 
 41 | IPv6 address selection
 42 | --------------------------------------------------------------------------------
 43 | 
 44 | When multiple IPv6 addresses are found on the interface or received from the
 45 | external command (e.g., when using --ipcmd), the following rules are used to
 46 | select the one to use:
 47 |     1. Only global unicast addresses (GUA) and unique local addresses (ULA) are
 48 |        considered.
 49 |     2. GUA addresses are preferred over ULA addresses.
 50 |     3. Unique EUI-64 addresses are preferred over randomly generated addresses.
 51 |     4. If priority subnets are specified, addresses from the subnet with the
 52 |        highest priority are selected. The priority is determined by the order of
 53 |        subnets specified on the command line or in the config file.
 54 | 
 55 | IPv4 address selection
 56 | --------------------------------------------------------------------------------
 57 | 
 58 | When multiple IPv4 addresses are found on the interface or received from the
 59 | external command (e.g., when using --ipcmd), the following rules are used to
 60 | select the one to use:
 61 |     1. All IPv4 addresses are considered.
 62 |     2. Public addresses are preferred over Shared Address Space (RFC 6598)
 63 |        addresses.
 64 |     3. Shared Address Space addresses are preferred over private addresses.
 65 |     4. Private addresses are preferred over loopback addresses.
 66 |     5. If priority subnets are specified, addresses from the subnet with the
 67 |        highest priority are selected. The priority is determined by the order of
 68 |        subnets specified on the command line or in the config file.
 69 | 
 70 | Non-public addresses are logged as warnings but are still used. They can be
 71 | useful in private networks or when using a VPN.
 72 | 
 73 | NOTE: Cloudflare doesn't allow proxying of records with non-public addresses.
 74 | 
 75 | Daemon mode
 76 | --------------------------------------------------------------------------------
 77 | 
 78 | By default, the program runs once and exits. This mode of operation can be
 79 | changed by setting the --run-every flag to a duration greater than 1m. In this
 80 | case, the program will run repeatedly, waiting the duration between runs. It
 81 | will stop if killed or if failed.
 82 | 
 83 | State file
 84 | --------------------------------------------------------------------------------
 85 | 
 86 | Setting --state-file makes the program to retain the previously used address
 87 | between runs to avoid unnecessary calls to the Cloudflare API.
 88 | 
 89 | The value is used as the state file path. When used with an empty value, the
 90 | state file is named after the interface name and the domains, and is stored
 91 | either in the current directory or in the directory specified by the
 92 | STATE_DIRECTORY environment variable.
 93 | 
 94 | The STATE_DIRECTORY environment variable is automatically set by systemd. It
 95 | can be set manually when running the program outside of systemd.
 96 | 
 97 | Multihost mode (EXPERIMENTAL)
 98 | --------------------------------------------------------------------------------
 99 | 
100 | In this mode, it is possible to assign multiple addresses to a single or
101 | multiple domains. For correct operation, this mode must be enabled on all hosts
102 | participating in the same domain and different host-ids must be specified for
103 | each host (see --host-id option). This mode is enabled by passing --multihost
104 | flag.
105 | 
106 | In the multihost mode, the program will manage only the DNS records that have
107 | the same host-id as the one specified on the command line or in the config file.
108 | If an existing record has no host-id but has the same address as the target one,
109 | it will be claimed by this host via setting the corresponding host-id. Any other
110 | records will be ignored. This allows multiple hosts to share the same domain
111 | without interfering with each other. The host-id is stored in the Cloudflare DNS
112 | comments field (see https://developers.cloudflare.com/dns/manage-dns-records/reference/record-attributes/).
113 | 
114 | Persistent configuration
115 | --------------------------------------------------------------------------------
116 | 
117 | The program can be configured using a config file. The default location is
118 | $HOME/.cloudflare-dynamic-dns.yaml. The config file location can be overridden
119 | using the --config flag. The config file format is YAML. The following options
120 | are supported (with example values):
121 | 
122 |     # === required fields
123 |     # either iface or ipcmd must be specified
124 |     iface: eth0
125 |     # ipcmd: curl -fsSL https://api6.ipify.org
126 |     token: cloudflare-api-token
127 |     domains:
128 |       - example.com
129 |       - "*.example.com"
130 |     # === optional fields
131 |     # --- mode
132 |     stack: ipv6
133 |     # --- UI
134 |     log-level: info
135 |     # --- logic
136 |     priority-subnets:
137 |       - 2001:db8::/32
138 |       - 2001:db8:1::/48
139 |     multihost: true
140 |     host-id: homelab-node-1
141 |     # --- DNS record details
142 |     proxy: enabled
143 |     ttl: 180
144 |     # --- daemon mode
145 |     run-every: 10m
146 |     state-file: /tmp/cfddns-eth0.state
147 | 
148 | Environment variables
149 | --------------------------------------------------------------------------------
150 | 
151 | The configuration options can be specified as environment variables. To make an
152 | environment variable name, prefix a flag name with CFDDNS_, replace dashes with
153 | underscores, and convert to uppercase. List values are specified as a single
154 | string containing elements separated by spaces.
155 | For example:
156 | 
157 |     CFDDNS_CONFIG=/path/to/config.yaml
158 |     CFDDNS_IFACE=eth0
159 |     CFDDNS_IPCMD='curl -fsSL https://api6.ipify.org'
160 |     CFDDNS_TOKEN=cloudflare-api-token
161 |     CFDDNS_DOMAINS='example.com *.example.com'
162 |     CFDDNS_STACK=ipv6
163 |     CFDDNS_LOG_LEVEL=info
164 |     CFDDNS_PRIORITY_SUBNETS='2001:db8::/32 2001:db8:1::/48'
165 |     CFDDNS_MULTIHOST=true
166 |     CFDDNS_HOST_ID=homelab-node-1
167 |     CFDDNS_PROXY=enabled
168 |     CFDDNS_TTL=180
169 |     CFDDNS_RUN_EVERY=10m
170 |     CFDDNS_STATE_FILE=/tmp/cfddns-eth0.state
171 | 
172 | Usage:
173 |   cloudflare-dynamic-dns [flags]
174 | 
175 | Flags:
176 |       --config string              config file (default is $HOME/.cloudflare-dynamic-dns.yaml)
177 |       --domains strings            Domain names to assign the address to.
178 |   -h, --help                       help for cloudflare-dynamic-dns
179 |       --host-id string             Unique host identifier. Must be specified in multihost mode.
180 |                                    Must be a valid DNS label. It is stored in the Cloudflare DNS comments field in
181 |                                    the format: "host-id (managed by cloudflare-dynamic-dns)"
182 |       --iface string               Network interface to look up for an address.
183 |       --ipcmd string               External command to run to get the address.
184 |       --log-level string           Sets logging level: trace, debug, info, warning, error, fatal, panic. (default "info")
185 |       --multihost                  Enable multihost mode.
186 |                                    In this mode it is possible to assign multiple addresses to a single domain.
187 |                                    For correct operation, this mode must be enabled on all participating hosts and
188 |                                    different host-ids must be specified for each host (see --host-id option).
189 |       --priority-subnets strings   Subnets to prefer over others.
190 |                                    If multiple addresses are found on the interface,
191 |                                    the one from the subnet with the highest priority is used.
192 |       --proxy string               Override proxy setting for created or updated DNS records.
193 |                                    If set to "auto", preserves the current state of an updated record.
194 |                                    Allowed values: "enabled", "disabled", "auto". (default "auto")
195 |       --run-every string           Re-run the program every N duration until it's killed.
196 |                                    The format is described at https://pkg.go.dev/time#ParseDuration.
197 |                                    The minimum duration is 1m. Examples: 4h30m15s, 5m.
198 |       --stack string               IP stack version: ipv4 or ipv6 (default "ipv6")
199 |       --state-file string          Enables usage of a state file.
200 |                                    In this mode, the previously used address is preserved
201 |                                    between runs to avoid unnecessary calls to Cloudflare API.
202 |                                    Automatically selects where to store the state file if no
203 |                                    value is specified. See the State file section in usage.
204 |       --token string               Cloudflare API token with DNS edit access rights.
205 |       --ttl int                    Time to live, in seconds, of the DNS record.
206 |                                    Must be between 60 and 86400, or 1 for 'automatic'. (default 1)
207 |   -v, --version                    version for cloudflare-dynamic-dns
208 | 
209 | 210 | 211 | ## Installation 212 | 213 | ### AUR 214 | 215 | There are two packages in AUR ( 216 | [1](https://aur.archlinux.org/packages/cloudflare-dynamic-dns/), 217 | [2](https://aur.archlinux.org/packages/cloudflare-dynamic-dns-bin/) 218 | ), that can be used on Arch-based distros: 219 | 220 | ```shell 221 | yay -S cloudflare-dynamic-dns 222 | # OR 223 | yay -S cloudflare-dynamic-dns-bin 224 | ``` 225 | 226 | ### Docker 227 | 228 | See the 229 | [container registry page](https://github.com/Zebradil/cloudflare-dynamic-dns/pkgs/container/cloudflare-dynamic-dns) 230 | for details. 231 | 232 | ```shell 233 | docker pull ghcr.io/zebradil/cloudflare-dynamic-dns:latest 234 | ``` 235 | 236 | ### DEB, RPM, APK 237 | 238 | See the [latest release page](https://github.com/Zebradil/cloudflare-dynamic-dns/releases/latest) for the full list of 239 | packages. 240 | 241 | ### Nix 242 | 243 | The package is available in the Nixpkgs repository under the name 244 | [`cloudflare-dynamic-dns`](https://search.nixos.org/packages?channel=unstable&show=cloudflare-dynamic-dns&from=0&size=50&sort=relevance&type=packages&query=cloudflare-dynamic-dns). 245 | 246 | ```shell 247 | nix-shell -p cloudflare-dynamic-dns 248 | ``` 249 | 250 | > [!NOTE] 251 | > The version in Nixpkgs is falling behind the latest release. If you need the latest version, use the flake. 252 | > 253 | > ```shell 254 | > nix shell 'github:Zebradil/cloudflare-dynamic-dns/main#cloudflare-dynamic-dns' 255 | > ``` 256 | 257 | ### Manual 258 | 259 | Download the archive for your OS from the [releases page](https://github.com/Zebradil/cloudflare-dynamic-dns/releases). 260 | 261 | Or get the source code and build the binary: 262 | 263 | ```shell 264 | git clone https://github.com/Zebradil/cloudflare-dynamic-dns.git 265 | # OR 266 | curl -sL https://github.com/Zebradil/cloudflare-dynamic-dns/archive/refs/heads/master.tar.gz | tar xz 267 | 268 | cd cloudflare-dynamic-dns-master 269 | go build -o cloudflare-dynamic-dns main.go 270 | ``` 271 | 272 | Now you can run `cloudflare-dynamic-dns` manually (see [Usage](#usage) section). 273 | 274 | If you want to do some automation with systemd, `cloudflare-dynamic-dns` has to be installed system-wide 275 | (it _is_ possible to run systemd timer and service without root privileges, but I do not provide ready-to-use configuration for this yet): 276 | 277 | ```shell 278 | sudo install -Dm755 cloudflare-dynamic-dns -t /usr/bin 279 | sudo install -Dm644 systemd/* -t /usr/lib/systemd/system 280 | sudo install -m700 -d /etc/cloudflare-dynamic-dns/config.d 281 | ``` 282 | 283 | ## Usage examples 284 | 285 | ### Run manually 286 | 287 | 0. Follow the steps from the [Installation](#installation) section. 288 | 1. Run `./cloudflare-dynamic-dns --domains 'example.com,*.example.com' --iface eth0 --token cloudflare-api-token` 289 | - NOTE: instead of compiling `cloudflare-dynamic-dns` binary, it can be replaced with `go run main.go` in the command above. 290 | 291 | Instead of specifying command line arguments, it is possible to create `~/.cloudflare-dynamic-dns.yaml` with the following structure: 292 | 293 | ```yaml 294 | iface: eth0 295 | token: cloudflare-api-token 296 | domains: 297 | - example.com 298 | - '*.example.com' 299 | ``` 300 | 301 | And then run `./cloudflare-dynamic-dns` (or `go run main.go`) without arguments. 302 | Or put the configuration in any place and specify it with `--config` flag: 303 | 304 | ```shell 305 | ./cloudflare-dynamic-dns --config /any/place/config.yaml 306 | ``` 307 | 308 | #### Run in a Docker container 309 | 310 | For the binary, the usage is the same as for the manual run. 311 | For the Docker container, we need to mount the configuration file into the container and provide access to the network stack of the host machine: 312 | 313 | ```shell 314 | docker run --rm \ 315 | --volume="/any/place/config.yaml:/config.yaml" \ 316 | --network=host \ 317 | ghcr.io/zebradil/cloudflare-dynamic-dns:latest \ 318 | --config=/config.yaml 319 | ``` 320 | 321 | To run the program in daemon mode, add `--run-every` flag (and `--detach` if you want to run it in the background): 322 | 323 | ```shell 324 | docker run --rm \ 325 | --volume="/any/place/config.yaml:/config.yaml" \ 326 | --network=host \ 327 | --detach \ 328 | ghcr.io/zebradil/cloudflare-dynamic-dns:latest \ 329 | --config=/config.yaml \ 330 | --run-every=5m 331 | ``` 332 | 333 | ### Systemd service and timer 334 | 335 | It is possible to run `cloudflare-dynamic-dns` periodically via systemd. 336 | This requires privileged access to the system. 337 | Make sure that required systemd files are installed (see [Installation](#installation) section for details). 338 | 339 | ```shell 340 | # 1. Create configuration file `/etc/cloudflare-dynamic-dns/config.d/.yaml` 341 | # For example (I use "example.com" as , replace the values according to your needs): 342 | sudo tee -a /etc/cloudflare-dynamic-dns/config.d/example.com.yaml < .goreleaser.yml 53 | 54 | docs:update-readme: 55 | desc: Update the README.md file 56 | cmds: 57 | - scripts/update_readme README.md 58 | -------------------------------------------------------------------------------- /cmd/cloudflare.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strings" 7 | 8 | cloudflare "github.com/cloudflare/cloudflare-go" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/weppos/publicsuffix-go/publicsuffix" 11 | ) 12 | 13 | func processDomain(api *cloudflare.API, domain string, addr string, cfg runConfig) { 14 | ctx := context.Background() 15 | recordType := getRecordType(cfg.stack) 16 | 17 | zoneName, err := publicsuffix.Domain(domain) 18 | if err != nil { 19 | log.WithError(err).Fatal("Couldn't get ZoneName") 20 | } else { 21 | log.WithField("zoneName", zoneName).Debug("Got ZoneName") 22 | } 23 | zoneID, err := api.ZoneIDByName(zoneName) 24 | if err != nil { 25 | log.WithError(err).Fatal("Couldn't get ZoneID") 26 | } 27 | 28 | desiredDNSRecord := cloudflare.DNSRecord{Type: recordType, Name: domain, Content: addr, TTL: cfg.ttl} 29 | if cfg.multihost { 30 | desiredDNSRecord.Comment = cfg.hostID 31 | } 32 | 33 | if cfg.proxy != "auto" { 34 | desiredDNSRecord.Proxied = Ptr(cfg.proxy == "enabled") 35 | } 36 | 37 | dnsRecordFilter := cloudflare.ListDNSRecordsParams{Type: recordType, Name: domain} 38 | existingDNSRecords, _, err := api.ListDNSRecords(ctx, cloudflare.ZoneIdentifier(zoneID), dnsRecordFilter) 39 | if err != nil { 40 | log.WithError(err).WithField("filter", dnsRecordFilter).Fatal("Couldn't get DNS records") 41 | } 42 | 43 | // If there are no existing records, create a new one and exit. 44 | if len(existingDNSRecords) == 0 { 45 | createNewDNSRecord(api, zoneID, desiredDNSRecord) 46 | return 47 | } 48 | 49 | // If there is already a record with the same address, we want to process it 50 | // first. Cloudflare API doesn't allow creating multiple records with the 51 | // same address, which may happen in the multihost mode. 52 | sort.Slice(existingDNSRecords, func(i, j int) bool { 53 | return existingDNSRecords[i].Content == addr 54 | }) 55 | for _, record := range existingDNSRecords { 56 | log.WithFields(log.Fields{ 57 | "comment": record.Comment, 58 | "content": record.Content, 59 | "domain": record.Name, 60 | "proxied": *record.Proxied, 61 | "ttl": record.TTL, 62 | "type": record.Type, 63 | }).Debug("Found DNS record") 64 | } 65 | 66 | sameHostFn := func(record cloudflare.DNSRecord, cfg runConfig) bool { 67 | recHost := strings.Split(record.Comment, " ")[0] 68 | cfgHost := strings.Split(cfg.hostID, " ")[0] 69 | return recHost == cfgHost 70 | } 71 | 72 | // Update the current record if it is either: 73 | // 1. has the same address as the desired record (proxied, ttl, or comment may have changed), 74 | // 2. has the same comment as the desired record and multihost is enabled (address and possibly proxied or ttl have changed), 75 | // 3. has empty comment and multihost is disabled (address and possibly proxied or ttl have changed). 76 | // NOTE: despite API returning empty Comment as `null`, cloudflare-go represents it as an empty string. 77 | // This can break in the future. 78 | shouldUpdateFn := func(record cloudflare.DNSRecord, cfg runConfig) bool { 79 | return record.Content == addr || 80 | cfg.multihost && sameHostFn(record, cfg) || 81 | !cfg.multihost && record.Comment == "" 82 | } 83 | 84 | // Look through all existing records. 85 | // Update the matching record if found, delete the rest. 86 | // If no matching record is found, create a new one. 87 | updated := false 88 | for _, record := range existingDNSRecords { 89 | if !updated && shouldUpdateFn(record, cfg) { 90 | updateDNSRecord(api, zoneID, record, desiredDNSRecord) 91 | updated = true 92 | continue 93 | } 94 | 95 | // In multihost mode, delete all records with the same host-id as the 96 | // current host. This should not happen during normal operation. 97 | if updated && record.Comment == cfg.hostID && cfg.multihost { 98 | log.WithField("record", record). 99 | Warn("Found another record with the same host-id as the current host, deleting it") 100 | deleteDNSRecord(api, zoneID, record) 101 | continue 102 | } 103 | 104 | // In single host mode, delete all other records. 105 | if updated && !cfg.multihost { 106 | deleteDNSRecord(api, zoneID, record) 107 | continue 108 | } 109 | } 110 | 111 | if !updated { 112 | createNewDNSRecord(api, zoneID, desiredDNSRecord) 113 | } 114 | } 115 | 116 | func getRecordType(stack IPStack) string { 117 | if stack == ipv4 { 118 | return "A" 119 | } 120 | if stack == ipv6 { 121 | return "AAAA" 122 | } 123 | log.WithField("stack", stack).Fatal("Invalid IP mode") 124 | return "" 125 | } 126 | 127 | func createNewDNSRecord(api *cloudflare.API, zoneID string, desiredDNSRecord cloudflare.DNSRecord) { 128 | ctx := context.Background() 129 | log.WithField("record", desiredDNSRecord).Info("Create new DNS record") 130 | _, err := api.CreateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.CreateDNSRecordParams{ 131 | Comment: desiredDNSRecord.Comment, 132 | Content: desiredDNSRecord.Content, 133 | Name: desiredDNSRecord.Name, 134 | Proxied: desiredDNSRecord.Proxied, 135 | TTL: desiredDNSRecord.TTL, 136 | Type: desiredDNSRecord.Type, 137 | }) 138 | if err != nil { 139 | log.WithError(err).Fatal("Couldn't create DNS record") 140 | } 141 | } 142 | 143 | func updateDNSRecord(api *cloudflare.API, zoneID string, oldRecord cloudflare.DNSRecord, newRecord cloudflare.DNSRecord) { 144 | ctx := context.Background() 145 | proxiedMatches := newRecord.Proxied == nil || 146 | (oldRecord.Proxied != nil && *oldRecord.Proxied == *newRecord.Proxied) 147 | if oldRecord.Content == newRecord.Content && 148 | proxiedMatches && 149 | oldRecord.TTL == newRecord.TTL && 150 | oldRecord.Comment == newRecord.Comment { 151 | log.WithField("record", oldRecord).Info("DNS record is up to date") 152 | return 153 | } 154 | 155 | log.WithFields(log.Fields{ 156 | "new": newRecord, 157 | "old": oldRecord, 158 | }).Info("Updating existing DNS record") 159 | _, err := api.UpdateDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), cloudflare.UpdateDNSRecordParams{ 160 | Comment: &newRecord.Comment, 161 | Content: newRecord.Content, 162 | ID: oldRecord.ID, 163 | Name: newRecord.Name, 164 | Proxied: newRecord.Proxied, 165 | TTL: newRecord.TTL, 166 | Type: newRecord.Type, 167 | }) 168 | if err != nil { 169 | log.WithError(err).WithFields(log.Fields{ 170 | "new": newRecord, 171 | "old": oldRecord, 172 | }).Fatal("Couldn't update DNS record") 173 | } 174 | } 175 | 176 | func deleteDNSRecord(api *cloudflare.API, zoneID string, record cloudflare.DNSRecord) { 177 | ctx := context.Background() 178 | log.WithField("record", record).Info("Deleting DNS record") 179 | err := api.DeleteDNSRecord(ctx, cloudflare.ZoneIdentifier(zoneID), record.ID) 180 | if err != nil { 181 | log.WithError(err).WithField("record", record).Fatal("Couldn't delete DNS record") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | cloudflare "github.com/cloudflare/cloudflare-go" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | const longDescription = ` 18 | Selects an address from the specified network interface or via an external 19 | command and updates A or AAAA records at Cloudflare for the configured domains. 20 | Supports both IPv4 and IPv6. 21 | 22 | Required configuration options 23 | -------------------------------------------------------------------------------- 24 | 25 | --iface: network interface name to look up for an address 26 | or 27 | --ipcmd: shell command to run to get the address, should return one address 28 | per line. Uses https://github.com/mvdan/sh as the shell. 29 | Examples: 30 | - curl -fsSL https://api6.ipify.org 31 | - echo -e "127.0.0.1\n127.0.0.2" 32 | 33 | --domains: one or more domain names to assign the address to 34 | --token: Cloudflare API token with edit access rights to the DNS zone 35 | 36 | IPv6 address selection 37 | -------------------------------------------------------------------------------- 38 | 39 | When multiple IPv6 addresses are found on the interface or received from the 40 | external command (e.g., when using --ipcmd), the following rules are used to 41 | select the one to use: 42 | 1. Only global unicast addresses (GUA) and unique local addresses (ULA) are 43 | considered. 44 | 2. GUA addresses are preferred over ULA addresses. 45 | 3. Unique EUI-64 addresses are preferred over randomly generated addresses. 46 | 4. If priority subnets are specified, addresses from the subnet with the 47 | highest priority are selected. The priority is determined by the order of 48 | subnets specified on the command line or in the config file. 49 | 50 | IPv4 address selection 51 | -------------------------------------------------------------------------------- 52 | 53 | When multiple IPv4 addresses are found on the interface or received from the 54 | external command (e.g., when using --ipcmd), the following rules are used to 55 | select the one to use: 56 | 1. All IPv4 addresses are considered. 57 | 2. Public addresses are preferred over Shared Address Space (RFC 6598) 58 | addresses. 59 | 3. Shared Address Space addresses are preferred over private addresses. 60 | 4. Private addresses are preferred over loopback addresses. 61 | 5. If priority subnets are specified, addresses from the subnet with the 62 | highest priority are selected. The priority is determined by the order of 63 | subnets specified on the command line or in the config file. 64 | 65 | Non-public addresses are logged as warnings but are still used. They can be 66 | useful in private networks or when using a VPN. 67 | 68 | NOTE: Cloudflare doesn't allow proxying of records with non-public addresses. 69 | 70 | Daemon mode 71 | -------------------------------------------------------------------------------- 72 | 73 | By default, the program runs once and exits. This mode of operation can be 74 | changed by setting the --run-every flag to a duration greater than 1m. In this 75 | case, the program will run repeatedly, waiting the duration between runs. It 76 | will stop if killed or if failed. 77 | 78 | State file 79 | -------------------------------------------------------------------------------- 80 | 81 | Setting --state-file makes the program to retain the previously used address 82 | between runs to avoid unnecessary calls to the Cloudflare API. 83 | 84 | The value is used as the state file path. When used with an empty value, the 85 | state file is named after the interface name and the domains, and is stored 86 | either in the current directory or in the directory specified by the 87 | STATE_DIRECTORY environment variable. 88 | 89 | The STATE_DIRECTORY environment variable is automatically set by systemd. It 90 | can be set manually when running the program outside of systemd. 91 | 92 | Multihost mode (EXPERIMENTAL) 93 | -------------------------------------------------------------------------------- 94 | 95 | In this mode, it is possible to assign multiple addresses to a single or 96 | multiple domains. For correct operation, this mode must be enabled on all hosts 97 | participating in the same domain and different host-ids must be specified for 98 | each host (see --host-id option). This mode is enabled by passing --multihost 99 | flag. 100 | 101 | In the multihost mode, the program will manage only the DNS records that have 102 | the same host-id as the one specified on the command line or in the config file. 103 | If an existing record has no host-id but has the same address as the target one, 104 | it will be claimed by this host via setting the corresponding host-id. Any other 105 | records will be ignored. This allows multiple hosts to share the same domain 106 | without interfering with each other. The host-id is stored in the Cloudflare DNS 107 | comments field (see https://developers.cloudflare.com/dns/manage-dns-records/reference/record-attributes/). 108 | 109 | Persistent configuration 110 | -------------------------------------------------------------------------------- 111 | 112 | The program can be configured using a config file. The default location is 113 | $HOME/.cloudflare-dynamic-dns.yaml. The config file location can be overridden 114 | using the --config flag. The config file format is YAML. The following options 115 | are supported (with example values): 116 | 117 | # === required fields 118 | # either iface or ipcmd must be specified 119 | iface: eth0 120 | # ipcmd: curl -fsSL https://api6.ipify.org 121 | token: cloudflare-api-token 122 | domains: 123 | - example.com 124 | - "*.example.com" 125 | # === optional fields 126 | # --- mode 127 | stack: ipv6 128 | # --- UI 129 | log-level: info 130 | # --- logic 131 | priority-subnets: 132 | - 2001:db8::/32 133 | - 2001:db8:1::/48 134 | multihost: true 135 | host-id: homelab-node-1 136 | # --- DNS record details 137 | proxy: enabled 138 | ttl: 180 139 | # --- daemon mode 140 | run-every: 10m 141 | state-file: /tmp/cfddns-eth0.state 142 | 143 | Environment variables 144 | -------------------------------------------------------------------------------- 145 | 146 | The configuration options can be specified as environment variables. To make an 147 | environment variable name, prefix a flag name with CFDDNS_, replace dashes with 148 | underscores, and convert to uppercase. List values are specified as a single 149 | string containing elements separated by spaces. 150 | For example: 151 | 152 | CFDDNS_CONFIG=/path/to/config.yaml 153 | CFDDNS_IFACE=eth0 154 | CFDDNS_IPCMD='curl -fsSL https://api6.ipify.org' 155 | CFDDNS_TOKEN=cloudflare-api-token 156 | CFDDNS_DOMAINS='example.com *.example.com' 157 | CFDDNS_STACK=ipv6 158 | CFDDNS_LOG_LEVEL=info 159 | CFDDNS_PRIORITY_SUBNETS='2001:db8::/32 2001:db8:1::/48' 160 | CFDDNS_MULTIHOST=true 161 | CFDDNS_HOST_ID=homelab-node-1 162 | CFDDNS_PROXY=enabled 163 | CFDDNS_TTL=180 164 | CFDDNS_RUN_EVERY=10m 165 | CFDDNS_STATE_FILE=/tmp/cfddns-eth0.state 166 | ` 167 | 168 | type IPStack string 169 | 170 | const ( 171 | ipv4 IPStack = "ipv4" 172 | ipv6 IPStack = "ipv6" 173 | ) 174 | 175 | type runConfig struct { 176 | domains []string 177 | hostID string 178 | iface string 179 | ipcmd string 180 | multihost bool 181 | prioritySubnets []string 182 | proxy string 183 | runEvery time.Duration 184 | stack IPStack 185 | stateFilepath string 186 | token string 187 | ttl int 188 | } 189 | 190 | var cfgFile string 191 | 192 | func init() { 193 | cobra.OnInitialize(initConfig) 194 | } 195 | 196 | func initConfig() { 197 | viper.SetEnvPrefix("CFDDNS") 198 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 199 | viper.AutomaticEnv() 200 | 201 | if cfgFile == "" { 202 | cfgFile = viper.GetString("config") 203 | } 204 | 205 | if cfgFile != "" { 206 | viper.SetConfigFile(cfgFile) 207 | } else { 208 | home, err := os.UserHomeDir() 209 | cobra.CheckErr(err) 210 | 211 | viper.AddConfigPath(home) 212 | viper.SetConfigType("yaml") 213 | viper.SetConfigName(".cloudflare-dynamic-dns") 214 | } 215 | 216 | if err := viper.ReadInConfig(); err == nil { 217 | log.Info("Using config file:", viper.ConfigFileUsed()) 218 | } 219 | } 220 | 221 | func NewRootCmd(version, commit, date string) *cobra.Command { 222 | rootCmd := &cobra.Command{ 223 | Use: "cloudflare-dynamic-dns", 224 | Short: "Updates A or AAAA records at Cloudflare according to the current address", 225 | Long: longDescription, 226 | Args: cobra.NoArgs, 227 | Version: fmt.Sprintf("%s, commit %s, built at %s", version, commit, date), 228 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 229 | level, err := log.ParseLevel(viper.GetString("log-level")) 230 | if err != nil { 231 | return err 232 | } 233 | log.Info(cmd.Name(), " version ", cmd.Version) 234 | log.Info("Setting log level to:", level) 235 | log.SetLevel(level) 236 | return nil 237 | }, 238 | Run: rootCmdRun, 239 | } 240 | 241 | rootCmd. 242 | PersistentFlags(). 243 | StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cloudflare-dynamic-dns.yaml)") 244 | 245 | rootCmd. 246 | Flags(). 247 | StringSlice("domains", []string{}, "Domain names to assign the address to.") 248 | 249 | rootCmd. 250 | Flags(). 251 | String("host-id", "", `Unique host identifier. Must be specified in multihost mode. 252 | Must be a valid DNS label. It is stored in the Cloudflare DNS comments field in 253 | the format: "host-id (managed by cloudflare-dynamic-dns)"`) 254 | 255 | rootCmd. 256 | Flags(). 257 | String("iface", "", "Network interface to look up for an address.") 258 | 259 | rootCmd. 260 | Flags(). 261 | String("ipcmd", "", "External command to run to get the address.") 262 | 263 | rootCmd. 264 | Flags(). 265 | String("log-level", "info", "Sets logging level: trace, debug, info, warning, error, fatal, panic.") 266 | 267 | rootCmd. 268 | Flags(). 269 | Bool("multihost", false, `Enable multihost mode. 270 | In this mode it is possible to assign multiple addresses to a single domain. 271 | For correct operation, this mode must be enabled on all participating hosts and 272 | different host-ids must be specified for each host (see --host-id option).`) 273 | 274 | rootCmd. 275 | Flags(). 276 | StringSlice("priority-subnets", []string{}, `Subnets to prefer over others. 277 | If multiple addresses are found on the interface, 278 | the one from the subnet with the highest priority is used.`) 279 | 280 | rootCmd. 281 | Flags(). 282 | String("proxy", "auto", `Override proxy setting for created or updated DNS records. 283 | If set to "auto", preserves the current state of an updated record. 284 | Allowed values: "enabled", "disabled", "auto".`) 285 | 286 | rootCmd. 287 | Flags(). 288 | String("run-every", "", `Re-run the program every N duration until it's killed. 289 | The format is described at https://pkg.go.dev/time#ParseDuration. 290 | The minimum duration is 1m. Examples: 4h30m15s, 5m.`) 291 | 292 | rootCmd. 293 | Flags(). 294 | String("stack", "ipv6", "IP stack version: ipv4 or ipv6") 295 | 296 | rootCmd. 297 | Flags(). 298 | String("state-file", "", `Enables usage of a state file. 299 | In this mode, the previously used address is preserved 300 | between runs to avoid unnecessary calls to Cloudflare API. 301 | Automatically selects where to store the state file if no 302 | value is specified. See the State file section in usage.`) 303 | 304 | rootCmd. 305 | Flags(). 306 | String("token", "", "Cloudflare API token with DNS edit access rights.") 307 | 308 | rootCmd. 309 | Flags(). 310 | Int("ttl", 1, `Time to live, in seconds, of the DNS record. 311 | Must be between 60 and 86400, or 1 for 'automatic'.`) 312 | 313 | err := viper.BindPFlags(rootCmd.Flags()) 314 | if err != nil { 315 | log.WithError(err).Fatal("Couldn't bind flags") 316 | } 317 | 318 | return rootCmd 319 | } 320 | 321 | func rootCmdRun(cmd *cobra.Command, args []string) { 322 | cfg := collectConfiguration() 323 | for { 324 | run(cfg) 325 | if cfg.runEvery == 0 { 326 | break 327 | } 328 | log.Info("Sleeping for ", cfg.runEvery) 329 | time.Sleep(cfg.runEvery) 330 | } 331 | } 332 | 333 | func collectConfiguration() runConfig { 334 | if viper.ConfigFileUsed() != "" { 335 | log.WithField("config", viper.ConfigFileUsed()).Debug("Using config file") 336 | checkConfigAccessMode(viper.ConfigFileUsed()) 337 | } else { 338 | log.Debug("No config file used") 339 | } 340 | 341 | var ( 342 | domains = viper.GetStringSlice("domains") 343 | hostID = viper.GetString("host-id") 344 | iface = viper.GetString("iface") 345 | ipcmd = viper.GetString("ipcmd") 346 | multihost = viper.GetBool("multihost") 347 | prioritySubnets = viper.GetStringSlice("priority-subnets") 348 | proxy = viper.GetString("proxy") 349 | runEvery = viper.GetString("run-every") 350 | sleepDuration = time.Duration(0) 351 | stack = IPStack(viper.GetString("stack")) 352 | stateFileEnabled = viper.IsSet("state-file") 353 | stateFilepath = viper.GetString("state-file") 354 | token = viper.GetString("token") 355 | ttl = viper.GetInt("ttl") 356 | ) 357 | 358 | if proxy != "auto" && proxy != "enabled" && proxy != "disabled" { 359 | log.WithField("proxy", proxy).Error("Invalid proxy setting, must be one of: auto, enabled, disabled. Using auto.") 360 | proxy = "auto" 361 | } 362 | 363 | if stack != ipv6 && stack != ipv4 { 364 | log.WithField("stack", stack).Error("Invalid IP mode, must be one of: ipv4, ipv6. Using ipv6.") 365 | stack = ipv6 366 | } 367 | 368 | if ttl < 60 || ttl > 86400 { 369 | // NOTE: 1 is a special value which means "use the default TTL" 370 | if ttl != 1 { 371 | log.WithFields(log.Fields{"ttl": ttl}).Warn("TTL must be between 60 and 86400; using Cloudflare's default") 372 | ttl = 1 373 | } 374 | } 375 | 376 | if runEvery != "" { 377 | parsedDuration, err := time.ParseDuration(runEvery) 378 | if err != nil { 379 | log.WithError(err).Fatal("Can't parse provided run-every duration") 380 | } 381 | if parsedDuration >= time.Minute { 382 | sleepDuration = parsedDuration 383 | } else { 384 | log.Warn("Provided run-every duration is less then 1 minute, will run just once") 385 | } 386 | } 387 | 388 | if stateFileEnabled && stateFilepath == "" { 389 | domainHash := fnv.New64a() 390 | domainHash.Write([]byte(strings.Join(domains, " "))) 391 | prefix := "none" 392 | if iface != "" { 393 | prefix = iface 394 | } 395 | stateFilepath = fmt.Sprintf("%s_%s_%x", prefix, stack, domainHash.Sum64()) 396 | // If STATE_DIRECTORY is set, use it as the state file directory, 397 | // otherwise use the current directory. 398 | if stateDir := os.Getenv("STATE_DIRECTORY"); stateDir != "" { 399 | stateFilepath = filepath.Join(stateDir, stateFilepath) 400 | } else { 401 | log.Info("STATE_DIRECTORY environment is not set, using the current directory for the state file") 402 | } 403 | } 404 | 405 | if multihost && hostID != "" { 406 | hostID = fmt.Sprintf("%s (managed by cloudflare-dynamic-dns)", hostID) 407 | } 408 | 409 | cfg := runConfig{ 410 | domains: domains, 411 | hostID: hostID, 412 | iface: iface, 413 | ipcmd: ipcmd, 414 | multihost: multihost, 415 | prioritySubnets: prioritySubnets, 416 | proxy: proxy, 417 | runEvery: sleepDuration, 418 | stack: stack, 419 | stateFilepath: stateFilepath, 420 | token: token, 421 | ttl: ttl, 422 | } 423 | 424 | logConfig(cfg) 425 | 426 | if token == "" { 427 | log.Fatal("No token specified") 428 | } 429 | 430 | if iface == "" && ipcmd == "" { 431 | log.Fatal("Neither iface nor ipcmd specified") 432 | } 433 | 434 | if iface != "" && ipcmd != "" { 435 | log.Fatal("Both iface and ipcmd specified, choose one") 436 | } 437 | 438 | if len(domains) == 0 { 439 | log.Fatal("No domains specified") 440 | } 441 | 442 | if multihost && hostID == "" { 443 | log.Fatal("Multihost mode requires host-id to be specified") 444 | } 445 | 446 | return cfg 447 | } 448 | 449 | func logConfig(cfg runConfig) { 450 | log.WithFields(log.Fields{ 451 | "domains": cfg.domains, 452 | "hostId": cfg.hostID, 453 | "iface": cfg.iface, 454 | "ipcmd": cfg.ipcmd, 455 | "multihost": cfg.multihost, 456 | "prioritySubnets": cfg.prioritySubnets, 457 | "proxy": cfg.proxy, 458 | "runEvery": cfg.runEvery, 459 | "stack": cfg.stack, 460 | "stateFilepath": cfg.stateFilepath, 461 | "token": fmt.Sprintf("[%d characters]", len(cfg.token)), 462 | "ttl": cfg.ttl, 463 | }).Debug("Configuration") 464 | } 465 | 466 | func run(cfg runConfig) { 467 | ip := "" 468 | ipMgr := newIPManager(cfg) 469 | 470 | if cfg.ipcmd != "" { 471 | ip = ipMgr.getIPFromCommand() 472 | } else { 473 | ip = ipMgr.getIPFromInterface() 474 | } 475 | 476 | if cfg.stateFilepath != "" && ip == ipMgr.getOldIP() { 477 | log.Info("The address hasn't changed, nothing to do") 478 | log.Info("To bypass this check run without the --state-file flag or remove the state file: ", cfg.stateFilepath) 479 | return 480 | } 481 | 482 | api, err := cloudflare.NewWithAPIToken(cfg.token) 483 | if err != nil { 484 | log.WithError(err).Fatal("Couldn't create API client") 485 | } 486 | 487 | for _, domain := range cfg.domains { 488 | log.Info("Processing domain: ", domain) 489 | processDomain(api, domain, ip, cfg) 490 | } 491 | 492 | if cfg.stateFilepath != "" { 493 | ipMgr.setOldIP(ip) 494 | } 495 | } 496 | 497 | func checkConfigAccessMode(configFilename string) { 498 | info, err := os.Stat(configFilename) 499 | if err != nil { 500 | log.WithError(err).Fatal("Can't get config file info") 501 | } 502 | log.WithField("mode", info.Mode()).Debug("Config file mode") 503 | if info.Mode()&0o011 != 0 { 504 | log.Warn("Config file should be accessible only by owner") 505 | } 506 | } 507 | 508 | // Ptr returns a pointer to the value passed as an argument. 509 | // This is a workaround for the lack of support for pointer literals in Go. 510 | // See https://stackoverflow.com/a/30716481/2227895 for more information. 511 | func Ptr[T any](v T) *T { 512 | return &v 513 | } 514 | -------------------------------------------------------------------------------- /cmd/ip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "os" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/zebradil/cloudflare-dynamic-dns/internal/execext" 13 | ) 14 | 15 | type ipStack interface { 16 | checkIPStack(net.IP) bool 17 | getBaseScore(net.IP) uint16 18 | logIP(net.IP) 19 | String() string 20 | } 21 | 22 | type ipManager struct { 23 | cfg runConfig 24 | ipStack 25 | } 26 | 27 | type ipv4Stack struct{} 28 | 29 | func (s ipv4Stack) String() string { 30 | return "IPv4" 31 | } 32 | 33 | type ipv6Stack struct{} 34 | 35 | func (s ipv6Stack) String() string { 36 | return "IPv6" 37 | } 38 | 39 | func newIPManager(cfg runConfig) ipManager { 40 | switch cfg.stack { 41 | case ipv4: 42 | return ipManager{cfg, ipv4Stack{}} 43 | case ipv6: 44 | return ipManager{cfg, ipv6Stack{}} 45 | default: 46 | log.WithField("stack", cfg.stack).Fatal("Unknown stack") 47 | return ipManager{} 48 | } 49 | } 50 | 51 | func (mgr ipManager) getIPFromCommand() string { 52 | var stdout bytes.Buffer 53 | var stderr bytes.Buffer 54 | opts := &execext.RunCommandOptions{ 55 | Command: mgr.cfg.ipcmd, 56 | Stdout: &stdout, 57 | Stderr: &stderr, 58 | } 59 | log.WithField("command", mgr.cfg.ipcmd).Debug("Running the command") 60 | if err := execext.RunCommand(context.Background(), opts); err != nil { 61 | log.WithError(err).WithFields(log.Fields{ 62 | "stdout": stdout.String(), 63 | "stderr": stderr.String(), 64 | }).Fatal("Couldn't get the address from the command") 65 | } 66 | log.WithFields(log.Fields{ 67 | "stdout": stdout.String(), 68 | "stderr": stderr.String(), 69 | }).Debug("Command output") 70 | IPs := []net.IP{} 71 | for _, line := range strings.Split(strings.TrimSpace(stdout.String()), "\n") { 72 | ip := net.ParseIP(strings.TrimSpace(line)) 73 | if ip == nil { 74 | log.WithField("address", line).Warn("Couldn't parse the address") 75 | continue 76 | } 77 | if mgr.checkIPStack(ip) { 78 | log.WithField("address", ip).Debug("Found a suitable address") 79 | IPs = append(IPs, ip) 80 | } else { 81 | log.WithFields(log.Fields{ 82 | "address": ip, 83 | "stack": mgr.ipStack, 84 | }).Debug("The address doesn't belong to the stack") 85 | } 86 | } 87 | if len(IPs) == 0 { 88 | log.Fatal("No suitable addresses found") 89 | } 90 | log.WithField("addresses", IPs).Debug("Found addresses") 91 | return mgr.selectOneFromIPs(IPs) 92 | } 93 | 94 | func (mgr ipManager) getIPFromInterface() string { 95 | return mgr.selectOneFromIPs(mgr.getAllStackIPs()) 96 | } 97 | 98 | func (mgr ipManager) selectOneFromIPs(ips []net.IP) string { 99 | ip := mgr.pickIP(ips) 100 | if ip == nil { 101 | log.Fatal("No suitable addresses found") 102 | } 103 | log.WithField("addresses", ips).Infof("Found %d public IP addresses, selected %s", len(ips), ip) 104 | mgr.logIP(ip) 105 | return ip.String() 106 | } 107 | 108 | func (mgr ipManager) getAllStackIPs() []net.IP { 109 | iface := mgr.cfg.iface 110 | netIface, err := net.InterfaceByName(iface) 111 | if err != nil { 112 | log.WithError(err).WithField("iface", iface).Fatal("Can't get the interface") 113 | } 114 | log.WithField("interface", netIface).Debug("Found the interface") 115 | 116 | addrs, err := netIface.Addrs() 117 | if err != nil { 118 | log.WithError(err).Fatal("Couldn't get interface addresses") 119 | } 120 | 121 | ips := []net.IP{} 122 | for _, addr := range addrs { 123 | ip, _, err := net.ParseCIDR(addr.String()) 124 | if err != nil { 125 | log.WithError(err).WithField("address", addr).Error("Couldn't parse address") 126 | continue 127 | } 128 | if mgr.checkIPStack(ip) { 129 | ips = append(ips, ip) 130 | } 131 | } 132 | return ips 133 | } 134 | 135 | // The score function evaluates the value of a given IP address and returns 136 | // a score of type uint64. 137 | // The higher the score, the more valuable the IP address. 138 | func (mgr ipManager) score(ip net.IP) uint64 { 139 | // Score format: 140 | // +------------+------------------+--------------+ 141 | // | Reserved | Priority Subnets | Address Type | 142 | // | (16 bits) | (32 bits) | (16 bits) | 143 | // +------------+------------------+--------------+ 144 | score := uint64(mgr.getBaseScore(ip)) 145 | 146 | // Scoring by priority subnet 147 | for order, subnet := range mgr.cfg.prioritySubnets { 148 | _, ipNet, err := net.ParseCIDR(subnet) 149 | if err != nil { 150 | log.WithError(err).WithField("subnet", subnet).Error("Couldn't parse subnet") 151 | continue 152 | } 153 | if ipNet.Contains(ip) { 154 | score += uint64(len(mgr.cfg.prioritySubnets)-order) << 16 155 | break 156 | } 157 | } 158 | return score 159 | } 160 | 161 | func (mgr ipManager) pickIP(ips []net.IP) net.IP { 162 | bestIPIdx := -1 163 | bestScore := uint64(0) // any address with score 0 will not be picked 164 | for idx, ip := range ips { 165 | score := mgr.score(ip) 166 | log.WithFields(log.Fields{ 167 | "address": ip, 168 | "score": score, 169 | }).Debug("Address score") 170 | if score > bestScore { 171 | bestIPIdx = idx 172 | bestScore = score 173 | } 174 | } 175 | if bestIPIdx < 0 { 176 | return nil 177 | } 178 | return ips[bestIPIdx] 179 | } 180 | 181 | func (s ipv6Stack) getBaseScore(ip net.IP) uint16 { 182 | score := uint16(1) 183 | 184 | if ip.To4() != nil { 185 | return 0 186 | } 187 | 188 | if !ip.IsGlobalUnicast() { 189 | return 0 190 | } 191 | 192 | if ipv6IsGUA(ip) { 193 | score += 0x8 194 | } 195 | 196 | if ipv6IsEUI64(ip) { 197 | score += 0x4 198 | } 199 | 200 | return score 201 | } 202 | 203 | func (s ipv4Stack) getBaseScore(ip net.IP) uint16 { 204 | score := uint16(1) 205 | 206 | if ip.To4() == nil { 207 | return 0 208 | } 209 | 210 | if ip.IsGlobalUnicast() { 211 | score += 0x8 212 | } 213 | 214 | if ipv4IsSAS(ip) { 215 | score += 0x4 216 | } 217 | 218 | if ip.IsPrivate() { 219 | score += 0x2 220 | } 221 | 222 | return score 223 | } 224 | 225 | func (mgr ipManager) getOldIP() string { 226 | ip, err := os.ReadFile(mgr.cfg.stateFilepath) 227 | if err != nil { 228 | log.WithError(err).Warn("Can't get old ipv6 address") 229 | return "INVALID" 230 | } 231 | return string(ip) 232 | } 233 | 234 | func (mgr ipManager) setOldIP(ip string) { 235 | err := os.WriteFile(mgr.cfg.stateFilepath, []byte(ip), 0o644) 236 | if err != nil { 237 | log.WithError(err).Error("Can't write state file") 238 | } 239 | } 240 | 241 | func (s ipv4Stack) checkIPStack(ip net.IP) bool { 242 | return ip.To4() != nil 243 | } 244 | 245 | func (s ipv6Stack) checkIPStack(ip net.IP) bool { 246 | return ip.To4() == nil 247 | } 248 | 249 | func (s ipv6Stack) logIP(ip net.IP) { 250 | if !ipv6IsEUI64(ip) { 251 | log.Warn("The selected address doesn't have a unique EUI-64, it may change frequently") 252 | } 253 | if !ipv6IsGUA(ip) { 254 | log.Warn("The selected address is not a GUA, it may not be routable") 255 | } 256 | } 257 | 258 | func (s ipv4Stack) logIP(ip net.IP) { 259 | if ipv4IsSAS(ip) { 260 | log.Warn("The selected address is in the Shared Address Space range") 261 | } 262 | if ip.IsPrivate() { 263 | log.Warn("The selected address is private, it may not be routable") 264 | } 265 | if ip.IsLoopback() { 266 | log.Warn("The selected address is a loopback address") 267 | } 268 | } 269 | 270 | // Custom function to check if an IPv6 address is a GUA. 271 | // net.IP.IsGlobalUnicast() returns true also for ULAs. 272 | func ipv6IsGUA(ip net.IP) bool { 273 | return ip[0]&0b11100000 == 0b00100000 274 | } 275 | 276 | // Custom function to check if an IPv6 address is generated using the EUI-64 format. 277 | // See RFC 4291, section 2.5.1. 278 | func ipv6IsEUI64(ip net.IP) bool { 279 | // If the seventh bit from the left of the Interface ID is 1, and "FF FE" is 280 | // found in the middle of the Interface ID, then the address is generated 281 | // using the EUI-64 format. 282 | return ip[8]&0b00000010 == 0b00000010 && ip[11] == 0xff && ip[12] == 0xfe 283 | } 284 | 285 | func ipv4IsSAS(ip net.IP) bool { 286 | // The Shared Address Space address range is 100.64.0.0/10. 287 | // See RFC 6598. 288 | return ip[0] == 100 && ip[1] >= 64 && ip[1] < 128 289 | } 290 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24@sha256:10c131810f80a4802c49cab0961bbe18a16f4bb2fb99ef16deaa23e4246fc817 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | 7 | RUN go mod download 8 | 9 | COPY . . 10 | RUN go build -o bin/cloudflare-dynamic-dns main.go 11 | 12 | ENTRYPOINT ["/app/bin/cloudflare-dynamic-dns"] 13 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1742206328, 24 | "narHash": "sha256-q+AQ///oMnyyFzzF4H9ShSRENt3Zsx37jTiRkLkXXE0=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "096478927c360bc18ea80c8274f013709cf7bdcd", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Dynamic DNS client for Cloudflare"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | flake-utils, 13 | ... 14 | }: 15 | flake-utils.lib.eachDefaultSystem (system: let 16 | pkgs = import nixpkgs {inherit system;}; 17 | package = import ./package.nix { 18 | inherit pkgs self; 19 | }; 20 | in { 21 | packages.default = package; 22 | packages.cloudflare-dynamic-dns = package; 23 | 24 | devShells.default = import ./shell.nix {inherit pkgs package;}; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/zebradil/cloudflare-dynamic-dns 2 | 3 | go 1.23 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/cloudflare/cloudflare-go v0.115.0 9 | github.com/spf13/cobra v1.9.1 10 | github.com/spf13/viper v1.20.1 11 | github.com/weppos/publicsuffix-go v0.40.2 12 | mvdan.cc/sh/v3 v3.11.0 13 | ) 14 | 15 | require ( 16 | github.com/fsnotify/fsnotify v1.8.0 // indirect 17 | github.com/hashicorp/hcl v1.0.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/magiconair/properties v1.8.7 // indirect 20 | github.com/mitchellh/mapstructure v1.5.0 // indirect 21 | github.com/sirupsen/logrus v1.9.3 22 | github.com/spf13/afero v1.12.0 // indirect 23 | github.com/spf13/cast v1.7.1 // indirect 24 | github.com/spf13/pflag v1.0.6 // indirect 25 | github.com/subosito/gotenv v1.6.0 // indirect 26 | golang.org/x/sys v0.30.0 // indirect 27 | golang.org/x/text v0.21.0 // indirect 28 | gopkg.in/ini.v1 v1.67.0 // indirect 29 | ) 30 | 31 | require ( 32 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 33 | github.com/goccy/go-json v0.10.5 // indirect 34 | github.com/google/go-querystring v1.1.0 // indirect 35 | github.com/muesli/cancelreader v0.2.2 // indirect 36 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 37 | github.com/sagikazarmark/locafero v0.7.0 // indirect 38 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 39 | github.com/sourcegraph/conc v0.3.0 // indirect 40 | go.uber.org/atomic v1.9.0 // indirect 41 | go.uber.org/multierr v1.9.0 // indirect 42 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 43 | golang.org/x/net v0.34.0 // indirect 44 | golang.org/x/sync v0.10.0 // indirect 45 | golang.org/x/term v0.29.0 // indirect 46 | golang.org/x/time v0.9.0 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 2 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 3 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 4 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 5 | github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= 6 | github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 8 | github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= 9 | github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 15 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 18 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 19 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 20 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 21 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 22 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 23 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 24 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 25 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 26 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 29 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 32 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 34 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= 36 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 37 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 38 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 39 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 40 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 41 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 42 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 43 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 44 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 46 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 47 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 48 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 49 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 50 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 51 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 52 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 53 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 54 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 55 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 58 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 60 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 61 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 62 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 63 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 64 | github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 65 | github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 66 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 67 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 68 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 69 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 70 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 71 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 72 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 73 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 74 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 75 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 76 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 77 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 78 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 79 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 80 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 81 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 82 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 83 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 84 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 85 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 86 | github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= 87 | github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 88 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 89 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 90 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 91 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 92 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 93 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 94 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 95 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 96 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 97 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 98 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 99 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 100 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 101 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 102 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 103 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 104 | github.com/weppos/publicsuffix-go v0.40.2 h1:LlnoSH0Eqbsi3ReXZWBKCK5lHyzf3sc1JEHH1cnlfho= 105 | github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko= 106 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 107 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 108 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 109 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 110 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 111 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 112 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 113 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 114 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 115 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 116 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 117 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 118 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 119 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 120 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 121 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 122 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 123 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 124 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 125 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 128 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 129 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 130 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 131 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 132 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 133 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 134 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 135 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 136 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 137 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 138 | golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= 139 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 143 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 144 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 145 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 146 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 147 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 157 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 159 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 160 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 161 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 162 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 163 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 164 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 165 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 166 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 167 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 168 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 169 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 170 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 171 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 172 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 173 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 174 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 175 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 176 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 177 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 178 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 179 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 181 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 182 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 183 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 184 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 185 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 186 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 187 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 188 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 189 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 190 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 191 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 192 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 193 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 194 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 195 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 196 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 197 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 198 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 199 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 200 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 202 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 203 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 204 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 205 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 206 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 207 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 208 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 209 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 210 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 211 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 212 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 213 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 214 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 215 | mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= 216 | mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= 217 | mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= 218 | mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= 219 | -------------------------------------------------------------------------------- /internal/execext/devnull.go: -------------------------------------------------------------------------------- 1 | package execext 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | var _ io.ReadWriteCloser = devNull{} 8 | 9 | type devNull struct{} 10 | 11 | func (devNull) Read(p []byte) (int, error) { return 0, io.EOF } 12 | func (devNull) Write(p []byte) (int, error) { return len(p), nil } 13 | func (devNull) Close() error { return nil } 14 | -------------------------------------------------------------------------------- /internal/execext/exec.go: -------------------------------------------------------------------------------- 1 | package execext 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "mvdan.cc/sh/v3/expand" 14 | "mvdan.cc/sh/v3/interp" 15 | "mvdan.cc/sh/v3/shell" 16 | "mvdan.cc/sh/v3/syntax" 17 | ) 18 | 19 | // RunCommandOptions is the options for the RunCommand func 20 | type RunCommandOptions struct { 21 | Command string 22 | Dir string 23 | Env []string 24 | PosixOpts []string 25 | BashOpts []string 26 | Stdin io.Reader 27 | Stdout io.Writer 28 | Stderr io.Writer 29 | } 30 | 31 | // ErrNilOptions is returned when a nil options is given 32 | var ErrNilOptions = errors.New("execext: nil options given") 33 | 34 | // RunCommand runs a shell command 35 | func RunCommand(ctx context.Context, opts *RunCommandOptions) error { 36 | if opts == nil { 37 | return ErrNilOptions 38 | } 39 | 40 | // Set "-e" or "errexit" by default 41 | opts.PosixOpts = append(opts.PosixOpts, "e") 42 | 43 | // Format POSIX options into a slice that mvdan/sh understands 44 | var params []string 45 | for _, opt := range opts.PosixOpts { 46 | if len(opt) == 1 { 47 | params = append(params, fmt.Sprintf("-%s", opt)) 48 | } else { 49 | params = append(params, "-o") 50 | params = append(params, opt) 51 | } 52 | } 53 | 54 | environ := opts.Env 55 | if len(environ) == 0 { 56 | environ = os.Environ() 57 | } 58 | 59 | r, err := interp.New( 60 | interp.Params(params...), 61 | interp.Env(expand.ListEnviron(environ...)), 62 | interp.ExecHandlers(execHandler), 63 | interp.OpenHandler(openHandler), 64 | interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr), 65 | dirOption(opts.Dir), 66 | ) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | parser := syntax.NewParser() 72 | 73 | // Run any shopt commands 74 | if len(opts.BashOpts) > 0 { 75 | shoptCmdStr := fmt.Sprintf("shopt -s %s", strings.Join(opts.BashOpts, " ")) 76 | shoptCmd, err := parser.Parse(strings.NewReader(shoptCmdStr), "") 77 | if err != nil { 78 | return err 79 | } 80 | if err := r.Run(ctx, shoptCmd); err != nil { 81 | return err 82 | } 83 | } 84 | 85 | // Run the user-defined command 86 | p, err := parser.Parse(strings.NewReader(opts.Command), "") 87 | if err != nil { 88 | return err 89 | } 90 | return r.Run(ctx, p) 91 | } 92 | 93 | // Expand is a helper to mvdan.cc/shell.Fields that returns the first field 94 | // if available. 95 | func Expand(s string) (string, error) { 96 | s = filepath.ToSlash(s) 97 | s = strings.ReplaceAll(s, " ", `\ `) 98 | s = strings.ReplaceAll(s, "&", `\&`) 99 | s = strings.ReplaceAll(s, "(", `\(`) 100 | s = strings.ReplaceAll(s, ")", `\)`) 101 | fields, err := shell.Fields(s, nil) 102 | if err != nil { 103 | return "", err 104 | } 105 | if len(fields) > 0 { 106 | return fields[0], nil 107 | } 108 | return "", nil 109 | } 110 | 111 | func execHandler(next interp.ExecHandlerFunc) interp.ExecHandlerFunc { 112 | return interp.DefaultExecHandler(15 * time.Second) 113 | } 114 | 115 | func openHandler(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { 116 | if path == "/dev/null" { 117 | return devNull{}, nil 118 | } 119 | return interp.DefaultOpenHandler()(ctx, path, flag, perm) 120 | } 121 | 122 | func dirOption(path string) interp.RunnerOption { 123 | return func(r *interp.Runner) error { 124 | err := interp.Dir(path)(r) 125 | if err == nil { 126 | return nil 127 | } 128 | 129 | // If the specified directory doesn't exist, it will be created later. 130 | // Therefore, even if `interp.Dir` method returns an error, the 131 | // directory path should be set only when the directory cannot be found. 132 | if absPath, _ := filepath.Abs(path); absPath != "" { 133 | if _, err := os.Stat(absPath); os.IsNotExist(err) { 134 | r.Dir = absPath 135 | return nil 136 | } 137 | } 138 | 139 | return err 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | govet: 4 | glob: "*.go" 5 | run: go vet ./... 6 | gofumpt: 7 | files: git diff --name-only --cached --diff-filter=AM 8 | glob: "*.go" 9 | run: gofumpt -l {files} | tee /dev/stderr | test $(wc -l) -eq 0 10 | golangci-lint: 11 | glob: "*.go" 12 | run: golangci-lint run 13 | commit-msg: 14 | commands: 15 | "lint commit message": 16 | run: commitlint --edit {1} 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/zebradil/cloudflare-dynamic-dns/cmd" 7 | ) 8 | 9 | var ( 10 | version = "dev" 11 | commit = "none" 12 | date = "unknown" 13 | ) 14 | 15 | func main() { 16 | log.SetLevel(log.DebugLevel) 17 | cmd := cmd.NewRootCmd(version, commit, date) 18 | if err := cmd.Execute(); err != nil { 19 | log.WithError(err).Fatal("Failed to execute command") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | self, 4 | }: let 5 | baseVersion = "4.3.19"; 6 | commit = self.shortRev or self.dirtyShortRev or "unknown"; 7 | version = "${baseVersion}-${commit}"; 8 | in 9 | pkgs.buildGoModule { 10 | pname = "cloudflare-dynamic-dns"; 11 | src = ./.; 12 | vendorHash = "sha256-YmkdehMpHHkQhMIKYP0HpcbP1WI+veSpNRe9nzhWcGM="; 13 | version = version; 14 | 15 | CGO_ENABLED = 0; 16 | ldflags = [ 17 | "-s" 18 | "-w" 19 | "-X=main.version=${baseVersion}" 20 | "-X=main.commit=${commit}" 21 | "-X=main.date=1970-01-01" 22 | ]; 23 | 24 | meta = { 25 | changelog = "https://github.com/Zebradil/cloudflare-dynamic-dns/blob/${baseVersion}/CHANGELOG.md"; 26 | description = "Dynamic DNS client for Cloudflare"; 27 | homepage = "https://github.com/Zebradil/cloudflare-dynamic-dns"; 28 | license = pkgs.lib.licenses.mit; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:best-practices", 5 | "helpers:pinGitHubActionDigestsToSemver", 6 | "schedule:nonOfficeHours", 7 | ], 8 | // Rebase PRs if needed even if there are commits from other bots 9 | gitIgnoredAuthors: [ 10 | "renovate[bot]@users.noreply.github.com", 11 | "github-actions[bot]@users.noreply.github.com", 12 | ], 13 | packageRules: [ 14 | { 15 | "matchFileNames": ["Dockerfile"], 16 | "matchDepNames": ["alpine"], 17 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 18 | "automerge": true 19 | }, 20 | { 21 | matchUpdateTypes: ["minor", "patch", "pin", "digest"], 22 | automerge: true, 23 | }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /scripts/update_readme: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | ROOT="$(git rev-parse --show-toplevel)" 6 | 7 | rdm:usage() { 8 | cat <"$tmp_file" 49 | 50 | echo '
' >>"$tmp_file"
51 |     (
52 |         cd "$ROOT"
53 |         go run main.go --help >>"$tmp_file"
54 |     )
55 |     echo '
' >>"$tmp_file" 56 | 57 | sed -n "/$end_marker/,\$p" "$file" >>"$tmp_file" 58 | cp "$tmp_file" "$file" 59 | rm "$tmp_file" 60 | } 61 | 62 | # Execute the main function if the script is not being sourced. 63 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then 64 | rdm:update "$@" 65 | fi 66 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | package, 4 | }: 5 | pkgs.mkShell { 6 | packages = 7 | (with pkgs; [ 8 | gnused 9 | go 10 | go-task 11 | gofumpt 12 | goimports-reviser 13 | golangci-lint 14 | goreleaser 15 | gosec 16 | nix-update 17 | ytt 18 | ]) 19 | ++ [ 20 | package 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /systemd/cloudflare-dynamic-dns@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Dynamic DNS for Cloudflare 3 | Documentation=https://github.com/Zebradil/cloudflare-dynamic-dns/blob/master/README.md 4 | Wants=network-online.target 5 | After=network-online.target nss-lookup.target 6 | 7 | 8 | [Service] 9 | Type=oneshot 10 | ExecStart=/usr/bin/cloudflare-dynamic-dns --state-file="" --config=/etc/cloudflare-dynamic-dns/config.d/%I.yaml 11 | StateDirectory=cloudflare-dynamic-dns 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /systemd/cloudflare-dynamic-dns@.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run cloudflare-dynamic-dns every 5 minutes and on boot 3 | 4 | [Timer] 5 | OnBootSec=1min 6 | OnUnitActiveSec=5m 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /vendir.lock.yml: -------------------------------------------------------------------------------- 1 | apiVersion: vendir.k14s.io/v1alpha1 2 | directories: 3 | - contents: 4 | - git: 5 | commitTitle: 'chore(deps): bump mvdan.cc/sh/v3 from 3.8.0 to 3.9.0 (#1765)...' 6 | sha: 0941de3318ba2cd8e8611acc4e225b0bea5c817b 7 | tags: 8 | - v3.38.0-17-g0941de33 9 | path: . 10 | path: internal/execext 11 | kind: LockConfig 12 | -------------------------------------------------------------------------------- /vendir.yml: -------------------------------------------------------------------------------- 1 | apiVersion: vendir.k14s.io/v1alpha1 2 | kind: Config 3 | directories: 4 | - path: internal/execext 5 | lazy: true 6 | contents: 7 | - path: . 8 | git: 9 | url: https://github.com/go-task/task 10 | ref: main 11 | includePaths: 12 | - internal/execext/* 13 | newRootPath: internal/execext 14 | --------------------------------------------------------------------------------