├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── .kodiak.toml ├── LICENSE.md ├── README.md ├── api.go ├── go.mod ├── go.sum └── main.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Update Go dependencies if they're found daily. 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "dependencies" 10 | - "🚀 merge it!" 11 | # Update Github Actions if they're found daily. 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | labels: 17 | - "dependencies" 18 | - "🚀 merge it!" 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | # Note the fetch-depth: 0 option is required for the change log to 15 | # work correctly with goreleaser. 16 | fetch-depth: 0 17 | - uses: actions/setup-go@v5 18 | with: 19 | go-version: "^1.23.5" 20 | - uses: golangci/golangci-lint-action@v6.3.2 21 | with: 22 | version: v1.63.4 23 | - name: Unit testing 24 | run: go test ./... 25 | - uses: goreleaser/goreleaser-action@v6.2.1 26 | with: 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: "^1.23.5" 17 | - uses: golangci/golangci-lint-action@v6.3.2 18 | with: 19 | version: v1.63.4 20 | - name: Unit testing 21 | run: go test ./... 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cloudflare-ddns -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | 3 | project_name: cloudflare-ddns 4 | version: 2 5 | builds: 6 | - binary: cloudflare-ddns 7 | goos: 8 | - "windows" 9 | - "darwin" 10 | - "linux" 11 | - "freebsd" 12 | - "openbsd" 13 | - "solaris" 14 | goarch: 15 | - "amd64" 16 | - "386" 17 | - "arm" 18 | - "arm64" 19 | goarm: 20 | - "7" 21 | ignore: 22 | - goos: "darwin" 23 | goarch: "386" 24 | - goos: "openbsd" 25 | goarch: "arm" 26 | 27 | brews: 28 | - repository: 29 | owner: wyattjoh 30 | name: homebrew-stable 31 | directory: Formula 32 | homepage: https://github.com/wyattjoh/cloudflare-ddns 33 | description: Cloudflare Dynamic DNS Updater 34 | test: | 35 | system "#{bin}/cloudflare-ddns -v" 36 | install: | 37 | bin.install "cloudflare-ddns" 38 | 39 | archives: 40 | - name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 41 | formats: ["tar.gz"] 42 | format_overrides: 43 | - goos: windows 44 | formats: ["zip"] 45 | files: 46 | - LICENSE.md 47 | - README.md 48 | 49 | release: 50 | github: 51 | owner: wyattjoh 52 | name: cloudflare-ddns 53 | -------------------------------------------------------------------------------- /.kodiak.toml: -------------------------------------------------------------------------------- 1 | # .kodiak.toml 2 | 3 | # Kodiak's configuration file should be placed at `.kodiak.toml` (repository 4 | # root) or `.github/.kodiak.toml`. 5 | # docs: https://kodiakhq.com/docs/config-reference 6 | 7 | # version is the only required setting in a kodiak config. 8 | # `1` is the only valid setting for this field. 9 | version = 1 10 | 11 | [merge] 12 | # Label to enable Kodiak to merge a PR. 13 | 14 | # By default, Kodiak will only act on PRs that have this label. You can disable 15 | # this requirement via `merge.require_automerge_label`. 16 | automerge_label = "🚀 merge it!" # default: "automerge" 17 | 18 | # Kodiak will not merge a PR with any of these labels. 19 | blocking_labels = ["don't merge"] # default: [], options: list of label names (e.g. ["wip"]) 20 | 21 | # Once a PR is merged, delete the branch. This option behaves like the GitHub 22 | # repository setting "Automatically delete head branches", which automatically 23 | # deletes head branches after pull requests are merged. 24 | delete_branch_on_merge = true # default: false 25 | 26 | # Don't wait for in-progress status checks on a PR to finish before updating the 27 | # branch. 28 | optimistic_updates = false # default: true 29 | 30 | [merge.message] 31 | # Strip HTML comments (``) from merge commit body. 32 | # This setting is useful for stripping HTML comments created by PR templates. 33 | # This option only applies when `merge.message.body_type = "markdown"`. 34 | strip_html_comments = true # default: false -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Wyatt Johnson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloudflare-ddns 2 | 3 | ![Test](https://github.com/wyattjoh/cloudflare-ddns/workflows/Test/badge.svg) 4 | 5 | This was a project designed to explore the [Cloudflare API](https://api.cloudflare.com/) 6 | through their official [Go Client](https://github.com/cloudflare/cloudflare-go). 7 | 8 | This application updates a given domain name to the current machine's ip address 9 | with the use of the https://ipify.org service. Alternative ip address services 10 | may be used if it returns the ip address of the current machine in plain text 11 | by overriding the configuration option. 12 | 13 | You can run this service in a cron job or a systemd timer, or once, up to you! 14 | 15 | ## Installation 16 | 17 | Install via with the Go toolchain to compile from source: 18 | 19 | ``` 20 | go get github.com/wyattjoh/cloudflare-ddns 21 | ``` 22 | 23 | Download pre-compiled binary on the [Releases Page](https://github.com/wyattjoh/cloudflare-ddns/releases/latest) for your Arch/OS. 24 | 25 | ### Installation Via Homebrew 26 | 27 | ``` 28 | brew install wyattjoh/stable/cloudflare-ddns 29 | ``` 30 | 31 | ## Configuration 32 | 33 | Configuration is specified in the environment or as command line arguments. 34 | 35 | - `--token` or `ENV['CF_API_TOKEN']` (_required_ if `--key` and `--email` not provided) - The API Token that has the Zone.DNS permission for the specific zone. 36 | - `--key` or `ENV['CF_API_KEY']` (_required_ if `--token` not provided) - specify the Global (not CA) Cloudflare API Key generated on the ["My Account" page](https://www.cloudflare.com/a/account/my-account). 37 | - `--email` or `ENV['CF_API_EMAIL']` (_required_ if `--token` not provided) - Email address associated with your Cloudflare account. 38 | - `--domain` or `ENV['CF_DOMAIN']` (_required_) - Comma separated domain names that should be updated. (i.e. mypage.example.com OR example.com) 39 | - `--ipendpoint` or `ENV['CF_IP_ENDPOINT']` (optional, default: `https://api.ipify.org`) - Alternative ip address service endpoint. 40 | 41 | Note: One of `--key` and `--email` or `--token` must be defined 42 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "strings" 8 | 9 | cloudflare "github.com/cloudflare/cloudflare-go" 10 | "github.com/pkg/errors" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func GetZoneID(ctx context.Context, api *cloudflare.API, domainName string) (string, error) { 15 | // Split the domain name by periods. 16 | splitDomainName := strings.Split(domainName, ".") 17 | 18 | // The domain name must be at least 2 elements, a name and a tld. 19 | if len(splitDomainName) < 2 { 20 | return "", errors.Errorf("%s did not contain a TLD", domainName) 21 | } 22 | 23 | // Extract the zone name from the domain name. This should be the last two 24 | // period delimitered strings. 25 | zoneName := strings.Join(splitDomainName[len(splitDomainName)-2:], ".") 26 | 27 | zoneID, err := api.ZoneIDByName(zoneName) 28 | if err != nil { 29 | return "", errors.Wrap(err, "could not find zone by name") 30 | } 31 | 32 | logrus.WithField("zoneID", zoneID).Debug("got zone id") 33 | 34 | return zoneID, nil 35 | } 36 | 37 | // GetRecord fetches the record from the Cloudflare api. 38 | func GetRecord(ctx context.Context, api *cloudflare.API, zoneID, domainName string) (*cloudflare.DNSRecord, error) { 39 | // Print zone details 40 | dnsRecords, _, err := api.ListDNSRecords(ctx, &cloudflare.ResourceContainer{ 41 | Type: cloudflare.ZoneType, 42 | Identifier: zoneID, 43 | }, cloudflare.ListDNSRecordsParams{ 44 | Name: domainName, 45 | }) 46 | if err != nil { 47 | return nil, errors.Wrap(err, "could not locate dns record for zone") 48 | } 49 | 50 | if len(dnsRecords) != 1 { 51 | return nil, errors.Errorf("Expected to find a single dns record, got %d", len(dnsRecords)) 52 | } 53 | 54 | // Capture the record id that we need to update. 55 | return &dnsRecords[0], nil 56 | } 57 | 58 | // GetCurrentIP gets the current machine's external IP address from the 59 | // https://ipify.org service. 60 | func GetCurrentIP(ipEndpoint string) (string, error) { 61 | req, err := http.NewRequest("GET", ipEndpoint, nil) 62 | if err != nil { 63 | return "", errors.Wrap(err, "could not create the request to the IP provider") 64 | } 65 | 66 | res, err := http.DefaultClient.Do(req) 67 | if err != nil { 68 | return "", errors.Wrap(err, "could not get the current IP from the provider") 69 | } 70 | defer res.Body.Close() 71 | 72 | data, err := io.ReadAll(res.Body) 73 | if err != nil { 74 | return "", errors.Wrap(err, "could not read the output from the provider") 75 | } 76 | 77 | // Update the IP address. 78 | return string(data), nil 79 | } 80 | 81 | // UpdateDomain updates a given domain in a zone to match the current ip address 82 | // of the machine. 83 | func UpdateDomain(ctx context.Context, api *cloudflare.API, zoneID, domainNames, ipEndpoint string) error { 84 | // Get our current IP address. 85 | newIP, err := GetCurrentIP(ipEndpoint) 86 | if err != nil { 87 | return errors.Wrap(err, "could not get the current IP address") 88 | } 89 | 90 | logrus.WithField("ip", newIP).Debug("got current IP address") 91 | 92 | // Split the domain names by comma, and range over them. 93 | splitDomainNames := strings.Split(domainNames, ",") 94 | for _, domainName := range splitDomainNames { 95 | // Get the record in question. 96 | record, err := GetRecord(ctx, api, zoneID, domainName) 97 | if err != nil { 98 | return errors.Wrap(err, "could not get the DNS record") 99 | } 100 | 101 | // Update the DNS record to include the new IP address. 102 | record.Content = newIP 103 | 104 | if _, err := api.UpdateDNSRecord(ctx, &cloudflare.ResourceContainer{ 105 | Type: cloudflare.ZoneType, 106 | Identifier: zoneID, 107 | }, cloudflare.UpdateDNSRecordParams{ 108 | ID: record.ID, 109 | Content: newIP, 110 | }); err != nil { 111 | return errors.Wrap(err, "could not update the DNS record") 112 | } 113 | 114 | // Log the update. 115 | logrus.WithFields(logrus.Fields{ 116 | "name": record.Name, 117 | "content": record.Content, 118 | }).Info("updated record") 119 | } 120 | 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wyattjoh/cloudflare-ddns 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/cloudflare/cloudflare-go v0.115.0 7 | github.com/pkg/errors v0.9.1 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/urfave/cli/v2 v2.27.5 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 14 | github.com/goccy/go-json v0.10.5 // indirect 15 | github.com/google/go-querystring v1.1.0 // indirect 16 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 17 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 18 | golang.org/x/net v0.34.0 // indirect 19 | golang.org/x/sys v0.29.0 // indirect 20 | golang.org/x/text v0.21.0 // indirect 21 | golang.org/x/time v0.9.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= 2 | github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 9 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 10 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 11 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 12 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 14 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 20 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 22 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 25 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 26 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 28 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 29 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 30 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 31 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 32 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 33 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 35 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 37 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 38 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 39 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 40 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cloudflare/cloudflare-go" 9 | "github.com/sirupsen/logrus" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | var ( 14 | version = "dev" 15 | commit = "none" 16 | date = "unknown" 17 | ) 18 | 19 | // Before will setup the logger. 20 | func Before(c *cli.Context) error { 21 | if c.Bool("json") { 22 | // Configure the JSON logger if enabled. 23 | logrus.SetFormatter(&logrus.JSONFormatter{}) 24 | } 25 | 26 | if c.Bool("debug") { 27 | // Set the debug log level if enabled. 28 | logrus.SetLevel(logrus.DebugLevel) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // Action will perform the update operation. 35 | func Action(c *cli.Context) error { 36 | ctx, cancel := context.WithCancel(context.Background()) 37 | defer cancel() 38 | 39 | var api *cloudflare.API 40 | 41 | var err error 42 | 43 | if c.String("token") != "" { 44 | api, err = cloudflare.NewWithAPIToken(c.String("token")) 45 | if err != nil { 46 | return cli.Exit(err.Error(), 1) 47 | } 48 | } else if c.String("key") != "" && c.String("email") != "" { 49 | api, err = cloudflare.New(c.String("key"), c.String("email")) 50 | if err != nil { 51 | return cli.Exit(err.Error(), 1) 52 | } 53 | } else { 54 | return cli.Exit("either --key and --email or --token must be defined", 1) 55 | } 56 | 57 | zoneID, err := GetZoneID(ctx, api, c.String("domain")) 58 | if err != nil { 59 | return cli.Exit(err.Error(), 1) 60 | } 61 | 62 | if err := UpdateDomain(ctx, api, zoneID, c.String("domain"), c.String("ipendpoint")); err != nil { 63 | return cli.Exit(err.Error(), 1) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func main() { 70 | app := cli.NewApp() 71 | app.Name = "cloudflare-ddns" 72 | app.Version = fmt.Sprintf("%v, commit %v, built at %v", version, commit, date) 73 | app.Flags = []cli.Flag{ 74 | &cli.StringFlag{ 75 | Name: "token", 76 | EnvVars: []string{"CF_API_TOKEN"}, 77 | Usage: "The API Token that has the Zone.DNS permission for the specific zone.", 78 | }, 79 | &cli.StringFlag{ 80 | Name: "key", 81 | EnvVars: []string{"CF_API_KEY"}, 82 | Usage: "The Global (not CA) Cloudflare API Key generated on the \"My Account\" page.", 83 | }, 84 | &cli.StringFlag{ 85 | Name: "email", 86 | EnvVars: []string{"CF_API_EMAIL"}, 87 | Usage: "Email address associated with your Cloudflare account.", 88 | }, 89 | &cli.StringFlag{ 90 | Name: "domain", 91 | Required: true, 92 | EnvVars: []string{"CF_DOMAIN"}, 93 | Usage: "Comma separated domain names that should be updated. (i.e. mypage.example.com OR example.com)", 94 | }, 95 | &cli.StringFlag{ 96 | Name: "ipendpoint", 97 | Value: "https://api.ipify.org/", 98 | EnvVars: []string{"CF_IP_ENDPOINT"}, 99 | Usage: "Alternative ip address service endpoint.", 100 | }, 101 | &cli.BoolFlag{ 102 | Name: "debug", 103 | Usage: "Enables debug logging.", 104 | }, 105 | &cli.BoolFlag{ 106 | Name: "json", 107 | Usage: "Enables JSON output for the logging.", 108 | }, 109 | } 110 | app.Before = Before 111 | app.Action = Action 112 | 113 | if err := app.Run(os.Args); err != nil { 114 | logrus.WithError(err).Fatal() 115 | } 116 | } 117 | --------------------------------------------------------------------------------