├── .github └── workflows │ ├── docker-publish.yml │ ├── go.yml │ └── golangci-lint.yml ├── .gitignore ├── .gitlab-ci.yml ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── ddns │ └── main.go ├── dns ├── dns.go └── dns_test.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── internal ├── app │ └── app.go ├── config │ ├── config.go │ └── config_test.go ├── ip │ ├── ip.go │ └── ip_test.go └── wire │ ├── wire.go │ └── wire_gen.go ├── notify └── notify.go └── utils └── utils.go /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | push: 10 | branches: [ "master" ] 11 | # Publish semver tags as releases. 12 | tags: [ 'v*.*.*' ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | env: 17 | # Use docker.io for Docker Hub if empty 18 | REGISTRY: ghcr.io 19 | # github.repository as / 20 | IMAGE_NAME: ${{ github.repository }} 21 | 22 | 23 | jobs: 24 | build: 25 | 26 | runs-on: ubuntu-latest 27 | permissions: 28 | contents: read 29 | packages: write 30 | # This is used to complete the identity challenge 31 | # with sigstore/fulcio when running outside of PRs. 32 | id-token: write 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | # Install the cosign tool except on PR 39 | # https://github.com/sigstore/cosign-installer 40 | - name: Install cosign 41 | if: github.event_name != 'pull_request' 42 | uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 43 | with: 44 | cosign-release: 'v2.2.4' 45 | 46 | # Set up BuildKit Docker container builder to be able to build 47 | # multi-platform images and export cache 48 | # https://github.com/docker/setup-buildx-action 49 | - name: Set up Docker Buildx 50 | uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 51 | 52 | # Login against a Docker registry except on PR 53 | # https://github.com/docker/login-action 54 | - name: Log into registry ${{ env.REGISTRY }} 55 | if: github.event_name != 'pull_request' 56 | uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 57 | with: 58 | registry: ${{ env.REGISTRY }} 59 | username: ${{ github.actor }} 60 | password: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | # Extract metadata (tags, labels) for Docker 63 | # https://github.com/docker/metadata-action 64 | - name: Extract Docker metadata 65 | id: meta 66 | uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 67 | with: 68 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 69 | 70 | # Build and push Docker image with Buildx (don't push on PR) 71 | # https://github.com/docker/build-push-action 72 | - name: Build and push Docker image 73 | id: build-and-push 74 | uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 75 | with: 76 | context: . 77 | push: ${{ github.event_name != 'pull_request' }} 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | cache-from: type=gha 81 | cache-to: type=gha,mode=max 82 | 83 | # Sign the resulting Docker image digest except on PRs. 84 | # This will only write to the public Rekor transparency log when the Docker 85 | # repository is public to avoid leaking data. If you would like to publish 86 | # transparency data even for private images, pass --force to cosign below. 87 | # https://github.com/sigstore/cosign 88 | - name: Sign the published Docker image 89 | if: ${{ github.event_name != 'pull_request' }} 90 | env: 91 | # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable 92 | TAGS: ${{ steps.meta.outputs.tags }} 93 | DIGEST: ${{ steps.build-and-push.outputs.digest }} 94 | # This step uses the identity token to provision an ephemeral certificate 95 | # against the sigstore community Fulcio instance. 96 | run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} 97 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.24 20 | 21 | - name: Build 22 | run: make build 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: '1.24' 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v6 25 | with: 26 | version: latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # You can override the included template(s) by including variable overrides 2 | # SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings 3 | # Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings 4 | # Note that environment variables can be set in several places 5 | # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence 6 | image: docker:latest 7 | services: 8 | - docker:dind 9 | stages: 10 | - build 11 | - test 12 | - deploy 13 | coverage: 14 | stage: test 15 | image: golang:1.16 16 | tags: 17 | - docker 18 | script: 19 | - go mod download 20 | - go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt 21 | sast: 22 | stage: test 23 | include: 24 | - template: Security/Dependency-Scanning.gitlab-ci.yml 25 | - template: Security/License-Scanning.gitlab-ci.yml 26 | - template: Security/SAST.gitlab-ci.yml 27 | - template: Security/Secret-Detection.gitlab-ci.yml 28 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | modules-download-mode: readonly 4 | tests: false 5 | issues: 6 | exclude-dirs: 7 | - pkg/ent 8 | - pkg/proto 9 | linters: 10 | disable-all: true 11 | fast: false 12 | enable: 13 | - bodyclose 14 | - dogsled 15 | - durationcheck 16 | - errcheck 17 | - copyloopvar 18 | - govet 19 | - gosimple 20 | - gofmt 21 | - goconst 22 | - goimports 23 | - mnd 24 | - gocyclo 25 | - ineffassign 26 | - lll 27 | - prealloc 28 | - revive 29 | - staticcheck 30 | - typecheck 31 | - unused 32 | - unconvert 33 | - whitespace 34 | - wastedassign 35 | 36 | # don't enable: 37 | # - asciicheck 38 | # - scopelint 39 | # - gochecknoglobals 40 | # - gocognit 41 | # - godot 42 | # - godox 43 | # - goerr113 44 | # - interfacer 45 | # - maligned 46 | # - nestif 47 | # - prealloc 48 | # - testpackage 49 | # - stylrcheck 50 | # - wsl 51 | 52 | linters-settings: 53 | whitespace: 54 | multi-func: true 55 | lll: 56 | line-length: 160 57 | mnd: 58 | # don't include the "operation", "argument" and "assign" 59 | checks: 60 | - case 61 | - condition 62 | - return 63 | goconst: 64 | ignore-tests: true 65 | gocyclo: 66 | # recommend 10-20 67 | min-complexity: 30 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | ARG ARG_GOPROXY 4 | ENV GOPROXY $ARG_GOPROXY 5 | 6 | WORKDIR /home/app 7 | COPY go.mod go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | COPY . . 12 | RUN make build 13 | 14 | 15 | FROM ghcr.io/orvice/go-runtime:master 16 | 17 | LABEL org.opencontainers.image.description "DDNS" 18 | 19 | ENV PROJECT_NAME ddns 20 | 21 | COPY --from=builder /home/app/bin/${PROJECT_NAME} . 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 orvice 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | APP_NAME=ddns 4 | APP_CMD_DIR=cmd/$(APP_NAME) 5 | APP_BINARY=bin/$(APP_NAME) 6 | APP_BINARY_UNIX=bin/$(APP_NAME)_unix_amd64 7 | 8 | all: build 9 | 10 | .PHONY: test 11 | test: ## test 12 | go test -v ./... 13 | 14 | 15 | .PHONY: build 16 | build: ## build 17 | CGO_ENABLED=0 go build -o $(APP_BINARY) -v cmd/$(APP_NAME)/main.go 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ddns 2 | 3 | Support dns: 4 | 5 | * cloudflare 6 | * aliyun 7 | 8 | 9 | ## Usage 10 | 11 | This application can be configured using environment variables: 12 | 13 | ### Required Environment Variables 14 | - `DOMAIN`: The domain name to update 15 | - `DNS_PROVIDER`: DNS provider to use (either "cloudflare" or "aliyun") 16 | 17 | ### Provider-specific Variables 18 | 19 | #### Cloudflare 20 | - `CF_TOKEN`: Cloudflare API token 21 | 22 | #### Aliyun 23 | - `ALIYUN_ACCESS_KEY_ID`: Aliyun Access Key ID 24 | - `ALIYUN_ACCESS_KEY_SECRET`: Aliyun Access Key Secret 25 | 26 | ### Optional Telegram Notification 27 | - `TELEGRAM_TOKEN`: Telegram bot token 28 | - `TELEGRAM_CHATID`: Telegram chat ID for notifications 29 | 30 | ### Example Usage 31 | 32 | Using Cloudflare: 33 | ```env 34 | DNS_PROVIDER=cloudflare 35 | DOMAIN=example.com 36 | CF_TOKEN=your_cloudflare_token 37 | ``` 38 | 39 | Using Aliyun: 40 | ```env 41 | DNS_PROVIDER=aliyun 42 | DOMAIN=example.com 43 | ALIYUN_ACCESS_KEY_ID=your_access_key_id 44 | ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret 45 | ``` 46 | 47 | With Telegram notifications: 48 | ```env 49 | TELEGRAM_TOKEN=your_bot_token 50 | TELEGRAM_CHATID=your_chat_id 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /cmd/ddns/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/orvice/ddns/internal/wire" 9 | ) 10 | 11 | var ( 12 | IPNotifyFormat = "[%s] ip changed, old IP: %s new IP: %s" 13 | ) 14 | 15 | func main() { 16 | app, err := wire.NewApp() 17 | if err != nil { 18 | slog.Error("init app error", "error", err) 19 | os.Exit(1) 20 | } 21 | app.Run(context.Background()) 22 | } 23 | -------------------------------------------------------------------------------- /dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "github.com/libdns/alidns" 5 | "github.com/libdns/cloudflare" 6 | "github.com/libdns/libdns" 7 | "github.com/orvice/ddns/internal/config" 8 | ) 9 | 10 | type LibDNS interface { 11 | libdns.RecordGetter 12 | libdns.RecordAppender 13 | libdns.RecordSetter 14 | } 15 | 16 | func New(conf *config.Config) LibDNS { 17 | switch conf.DNSProvider { 18 | case "cloudflare": 19 | return NewCloudFlare(conf) 20 | case "aliyun": 21 | return NewAliyun(conf) 22 | } 23 | return NewCloudFlare(conf) 24 | } 25 | 26 | // cloudflare 27 | func NewCloudFlare(conf *config.Config) LibDNS { 28 | provider := cloudflare.Provider{APIToken: conf.CFToken} 29 | return &provider 30 | } 31 | 32 | // aliyun 33 | func NewAliyun(conf *config.Config) LibDNS { 34 | provider := alidns.Provider{ 35 | AccKeyID: conf.AliyunAccessKeyID, 36 | AccKeySecret: conf.AliyunAccessKeySecret, 37 | } 38 | return &provider 39 | } 40 | -------------------------------------------------------------------------------- /dns/dns_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/libdns/cloudflare" 9 | ) 10 | 11 | func TestCloudFlare(t *testing.T) { 12 | provider := cloudflare.Provider{APIToken: os.Getenv("CF_TOKEN")} 13 | records, err := provider.GetRecords(context.Background(), os.Getenv("CF_ZONE")) 14 | if err != nil { 15 | t.Log(err) 16 | return 17 | } 18 | t.Log(records) 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | ddns: 5 | image: orvice/ddns 6 | restart: always 7 | volumes: 8 | - ./log:/var/log 9 | environment: 10 | - DOMAIN= 11 | - CF_API_KEY= 12 | - CF_API_EMAIL= 13 | - TELEGRAM_TOKEN= 14 | - TELEGRAM_CHATID= 15 | container_name: ddns-auto 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/orvice/ddns 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 9 | github.com/google/wire v0.6.0 10 | github.com/hashicorp/go-retryablehttp v0.7.7 11 | github.com/libdns/alidns v1.0.3 12 | github.com/libdns/cloudflare v0.1.1 13 | github.com/libdns/libdns v0.2.2 14 | github.com/lmittmann/tint v1.0.5 15 | github.com/spf13/viper v1.18.0 16 | github.com/stretchr/testify v1.9.0 17 | github.com/weppos/publicsuffix-go v0.40.2 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 22 | github.com/fsnotify/fsnotify v1.7.0 // indirect 23 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 24 | github.com/hashicorp/hcl v1.0.0 // indirect 25 | github.com/magiconair/properties v1.8.7 // indirect 26 | github.com/mitchellh/mapstructure v1.5.0 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 28 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 29 | github.com/sagikazarmark/locafero v0.4.0 // indirect 30 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 31 | github.com/sourcegraph/conc v0.3.0 // indirect 32 | github.com/spf13/afero v1.11.0 // indirect 33 | github.com/spf13/cast v1.6.0 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | github.com/subosito/gotenv v1.6.0 // indirect 36 | go.uber.org/atomic v1.9.0 // indirect 37 | go.uber.org/multierr v1.9.0 // indirect 38 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 39 | golang.org/x/net v0.38.0 // indirect 40 | golang.org/x/sys v0.31.0 // indirect 41 | golang.org/x/text v0.23.0 // indirect 42 | gopkg.in/ini.v1 v1.67.0 // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 10 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 11 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 12 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 13 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 14 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 15 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 16 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 17 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 19 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 20 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 21 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 24 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 25 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q= 28 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 29 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 30 | github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= 31 | github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= 32 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 33 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 34 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 35 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 36 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 37 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 41 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 42 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= 45 | github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= 46 | github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= 47 | github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU= 48 | github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= 49 | github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= 50 | github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 51 | github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= 52 | github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 53 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 54 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 55 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 56 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 57 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 58 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 59 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 60 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 61 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 62 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 67 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 68 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 69 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 70 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 71 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 72 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 73 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 74 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 75 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 79 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 80 | github.com/spf13/viper v1.18.0 h1:pN6W1ub/G4OfnM+NR9p7xP9R6TltLUzp5JG9yZD3Qg0= 81 | github.com/spf13/viper v1.18.0/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= 82 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 83 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 84 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 85 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 86 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 87 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 89 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 90 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 91 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 92 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 93 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 94 | github.com/weppos/publicsuffix-go v0.40.2 h1:LlnoSH0Eqbsi3ReXZWBKCK5lHyzf3sc1JEHH1cnlfho= 95 | github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko= 96 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 97 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 98 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 99 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 100 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 101 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 102 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 103 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 104 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 105 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 106 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 107 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 108 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 109 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 110 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 111 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 112 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 113 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 114 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 115 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 116 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 117 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 118 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 119 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 120 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 121 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 122 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 123 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 124 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 125 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 126 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 127 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 128 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 129 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 130 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 131 | golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= 132 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 136 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 137 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 138 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 149 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 150 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 151 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 152 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 153 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 154 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 155 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 156 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 157 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 158 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 159 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 160 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 161 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 162 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 163 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 164 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 165 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 166 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 167 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 168 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 169 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 170 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 171 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 172 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 173 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 174 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 175 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 176 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 177 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 178 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 179 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 180 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 181 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 182 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 183 | golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 184 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 185 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 186 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 187 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 188 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 189 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 190 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 191 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 192 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 194 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 195 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 196 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/libdns/libdns" 12 | "github.com/orvice/ddns/dns" 13 | "github.com/orvice/ddns/internal/config" 14 | "github.com/orvice/ddns/internal/ip" 15 | "github.com/orvice/ddns/notify" 16 | ) 17 | 18 | var ( 19 | IPNotifyFormat = "[%s] ip changed, old IP: %s new IP: %s" 20 | ) 21 | 22 | type App struct { 23 | logger *slog.Logger 24 | config *config.Config 25 | dnsProvider dns.LibDNS 26 | ipGetter ip.Getter 27 | notifier notify.Notifier 28 | } 29 | 30 | func New(logger *slog.Logger, config *config.Config, dnsProvider dns.LibDNS, ipGetter ip.Getter, notifier notify.Notifier) *App { 31 | return &App{ 32 | logger: logger, 33 | config: config, 34 | dnsProvider: dnsProvider, 35 | ipGetter: ipGetter, 36 | notifier: notifier, 37 | } 38 | } 39 | 40 | func (a *App) Run(ctx context.Context) { 41 | for { 42 | select { 43 | case <-ctx.Done(): 44 | return 45 | default: 46 | err := a.updateIP(ctx) 47 | if err != nil { 48 | a.logger.Error("update ip error", "error", err.Error()) 49 | os.Exit(1) 50 | } 51 | time.Sleep(time.Minute * 3) 52 | } 53 | } 54 | } 55 | 56 | func (a *App) updateIP(ctx context.Context) error { 57 | ip, err := a.ipGetter.GetIP() 58 | if err != nil { 59 | a.logger.Error("Get ip error", "error", err) 60 | return err 61 | } 62 | 63 | name, zone := zoneFromDomain(a.config.Domain) 64 | a.logger.Info("zone from domain", 65 | "name", name, 66 | "zone", zone) 67 | 68 | currentIP, err := a.dnsProvider.GetRecords(ctx, zone) 69 | if err != nil { 70 | a.logger.Error("Get records error", "error", err) 71 | return err 72 | } 73 | 74 | var found bool 75 | var record *libdns.Record 76 | for _, r := range currentIP { 77 | if r.Name == name { 78 | found = true 79 | record = &r 80 | break 81 | } 82 | } 83 | if found { 84 | if record.Value == ip { 85 | a.logger.Info("ip is same, skip update", "ip", ip) 86 | return nil 87 | } 88 | oldIP := record.Value 89 | record.Value = ip 90 | _, err = a.dnsProvider.SetRecords(ctx, zone, []libdns.Record{ 91 | *record, 92 | }) 93 | if err != nil { 94 | a.logger.Error("Set records error", "error", err) 95 | return err 96 | } 97 | _ = a.notifier.Send(ctx, fmt.Sprintf(IPNotifyFormat, a.config.Domain, oldIP, ip)) 98 | } else { 99 | _, err = a.dnsProvider.AppendRecords(ctx, zone, []libdns.Record{ 100 | { 101 | Name: name, 102 | Value: ip, 103 | Type: "A", 104 | }, 105 | }) 106 | if err != nil { 107 | a.logger.Error("Append records error", "error", err) 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | // zoneFromDomain return zone and domain 116 | func zoneFromDomain(domain string) (string, string) { 117 | arr := strings.SplitN(domain, ".", 2) 118 | if len(arr) == 1 { 119 | return "", "" 120 | } 121 | return arr[0], arr[1] 122 | } 123 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | type Config struct { 8 | DNSProvider string `mapstructure:"DNS_PROVIDER"` 9 | Domain string `mapstructure:"DOMAIN"` 10 | TelegramChatID int64 `mapstructure:"TELEGRAM_CHATID"` 11 | TelegramToken string `mapstructure:"TELEGRAM_TOKEN"` 12 | 13 | CFToken string `mapstructure:"CF_TOKEN"` 14 | 15 | AliyunAccessKeyID string `mapstructure:"ALIYUN_ACCESS_KEY_ID"` 16 | AliyunAccessKeySecret string `mapstructure:"ALIYUN_ACCESS_KEY_SECRET"` 17 | } 18 | 19 | func New() (*Config, error) { 20 | path := "." 21 | viper.AddConfigPath(path) 22 | viper.SetConfigName("app") 23 | 24 | viper.SetConfigType("env") 25 | viper.AutomaticEnv() 26 | // viper.ReadInConfig() 27 | var config Config 28 | err := viper.Unmarshal(&config) 29 | return &config, err 30 | } 31 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoadConfig(t *testing.T) { 11 | os.Setenv("DOMAIN", "example.com") 12 | os.Setenv("TELEGRAM_CHATID", "1234567890") 13 | conf, err := New() 14 | assert.Nil(t, err) 15 | 16 | t.Log(conf) 17 | assert.Equal(t, "example.com", conf.Domain) 18 | assert.Equal(t, int64(1234567890), conf.TelegramChatID) 19 | } 20 | -------------------------------------------------------------------------------- /internal/ip/ip.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log/slog" 7 | 8 | "github.com/hashicorp/go-retryablehttp" 9 | ) 10 | 11 | const ( 12 | ipConfigCoAddr = "https://ifconfig.co/json" 13 | ) 14 | 15 | type Response struct { 16 | IP string `json:"ip"` 17 | } 18 | 19 | type Getter interface { 20 | GetIP() (string, error) 21 | } 22 | 23 | type IfconfigCo struct { 24 | } 25 | 26 | func NewIfGetter() Getter { 27 | return new(IfconfigCo) 28 | } 29 | 30 | func (i *IfconfigCo) GetIP() (string, error) { 31 | logger := slog.Default() 32 | retryClient := retryablehttp.NewClient() 33 | retryClient.RetryMax = 3 34 | resp, err := retryClient.Get(ipConfigCoAddr) 35 | if err != nil { 36 | logger.Error("get ip error", "error", err) 37 | return "", err 38 | } 39 | defer resp.Body.Close() 40 | 41 | s, err := io.ReadAll(resp.Body) 42 | if err != nil { 43 | logger.Error("read body error", "error", err) 44 | return "", err 45 | } 46 | var ret Response 47 | err = json.Unmarshal(s, &ret) 48 | if err != nil { 49 | logger.Error("unmarshal error", "error", err) 50 | return "", err 51 | } 52 | logger.Info("get ip", "ip", ret.IP) 53 | return ret.IP, nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/ip/ip_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetIP(t *testing.T) { 8 | ip, err := NewIfGetter().GetIP() 9 | t.Log(ip, err) 10 | } 11 | -------------------------------------------------------------------------------- /internal/wire/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package wire 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/orvice/ddns/dns" 9 | "github.com/orvice/ddns/internal/app" 10 | "github.com/orvice/ddns/internal/config" 11 | "github.com/orvice/ddns/internal/ip" 12 | "github.com/orvice/ddns/notify" 13 | "github.com/orvice/ddns/utils" 14 | ) 15 | 16 | func NewApp() (*app.App, error) { 17 | wire.Build(app.New, config.New, dns.New, ip.NewIfGetter, utils.NewLogger, notify.NewTelegramNotifier) 18 | return &app.App{}, nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/wire/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package wire 8 | 9 | import ( 10 | "github.com/orvice/ddns/dns" 11 | "github.com/orvice/ddns/internal/app" 12 | "github.com/orvice/ddns/internal/config" 13 | "github.com/orvice/ddns/internal/ip" 14 | "github.com/orvice/ddns/notify" 15 | "github.com/orvice/ddns/utils" 16 | ) 17 | 18 | // Injectors from wire.go: 19 | 20 | func NewApp() (*app.App, error) { 21 | logger := utils.NewLogger() 22 | configConfig, err := config.New() 23 | if err != nil { 24 | return nil, err 25 | } 26 | libDNS := dns.New(configConfig) 27 | getter := ip.NewIfGetter() 28 | notifier, err := notify.NewTelegramNotifier(configConfig) 29 | if err != nil { 30 | return nil, err 31 | } 32 | appApp := app.New(logger, configConfig, libDNS, getter, notifier) 33 | return appApp, nil 34 | } 35 | -------------------------------------------------------------------------------- /notify/notify.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 8 | "github.com/orvice/ddns/internal/config" 9 | ) 10 | 11 | var notifiers []Notifier 12 | 13 | type Notifier interface { 14 | Send(ctx context.Context, s string) error 15 | } 16 | 17 | var _ Notifier = new(TelegramNotifier) 18 | 19 | type TelegramNotifier struct { 20 | bot *tgbotapi.BotAPI 21 | token string 22 | chatID int64 23 | } 24 | 25 | func NewTelegramNotifier(conf *config.Config) (Notifier, error) { 26 | bot, err := tgbotapi.NewBotAPI(conf.TelegramToken) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &TelegramNotifier{ 31 | bot: bot, 32 | token: conf.TelegramToken, 33 | chatID: conf.TelegramChatID, 34 | }, nil 35 | } 36 | 37 | func (t *TelegramNotifier) Send(_ context.Context, s string) error { 38 | msg := tgbotapi.NewMessage(t.chatID, s) 39 | resp, err := t.bot.Send(msg) 40 | if err != nil { 41 | slog.Error("send message error", "error", err.Error()) 42 | return err 43 | } 44 | slog.Info("send message success", "resp", resp) 45 | return nil 46 | } 47 | 48 | func Init() { 49 | notifiers = make([]Notifier, 0) 50 | } 51 | 52 | func AddNotifier(n Notifier) { 53 | notifiers = append(notifiers, n) 54 | } 55 | 56 | func Notify(ctx context.Context, s string) { 57 | for _, n := range notifiers { 58 | _ = n.Send(ctx, s) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/lmittmann/tint" 8 | "github.com/weppos/publicsuffix-go/publicsuffix" 9 | ) 10 | 11 | func GetDomainSuffix(domain string) string { 12 | s, _ := publicsuffix.Domain(domain) 13 | return s 14 | } 15 | 16 | func NewLogger() *slog.Logger { 17 | w := os.Stderr 18 | logger := slog.New( 19 | tint.NewHandler(w, &tint.Options{ 20 | ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { 21 | if a.Key == slog.TimeKey && len(groups) == 0 { 22 | return slog.Attr{} 23 | } 24 | return a 25 | }, 26 | }), 27 | ) 28 | return logger 29 | } 30 | --------------------------------------------------------------------------------