Skip to content

Commit

Permalink
release 0.1
Browse files Browse the repository at this point in the history
Signed-off-by: zerjioang <[email protected]>
  • Loading branch information
zerjioang committed Oct 20, 2021
0 parents commit 48e4b21
Show file tree
Hide file tree
Showing 21 changed files with 1,151 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# old tokens of cf
secret.txt
dist/
cfagent
\.idea
60 changes: 60 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
project_name: cfagent
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- binary: "bin/cfagent"
# Custom flags templates.
- flags:
- -v
- -trimpath
- asmflags:
- all=-trimpath={{.Env.GOPATH}}
- gcflags:
- all=-trimpath={{.Env.GOPATH}}
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`.
- ldflags:
- -s -extldflags=-static -w -X main.build={{.Version}}
- tags:
- osusergo
- netgo
- env:
- CGO_ENABLED=0
- goos:
- linux
- freebsd
- windows
- darwin
- goarch:
- amd64
- arm
- arm64
- 386
- goarm:
- 6
- 7
- ignore:
- goos: windows
goarch: arm64
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Cloudflare DDNS Agent

> This is an unofficial development. Cloudflare has not official support for it.
## Goal

It updates your device (raspberr pi, beaglebone, linux, container, vm, etc) IP address on a Dynamic
DNS provider like Cloudflare so that you can forget about device IP and use it a DNS name.

## Requirements

* A owned DNS.
* A Cloudflare account.
* A Token created using Cloudflare dashboard to allow this application to edit your DNS Zone info.

## Usage

```bash
X_CF_AGENT_TOKEN=yourtoken \
X_CF_AGENT_ZONE=example.com \
X_CF_AGENT_DNS_A_RECORD=rpi \
cfagent update
```

Previous command will try to update the DNS A Record of rpi.example.com with the host actual public IP

### Customization of DDNS Cloudflare Agent

* X_CF_AGENT_TOKEN: token provided by Cloudflare in order to authenticate API calls
* X_CF_AGENT_ZONE: name of the DNS zone you want to edit
* X_CF_AGENT_DNS_A_RECORD: name of the DNS Record name you want to edit.

## Troubleshooting

### API Token must not be empty

```bash
2021/10/20 20:27:09 Updating device IP. Please wait...
2021/10/20 20:27:09 Requesting IP check for: .
2021/10/20 20:27:09 Reading current device IP. Please wait...
2021/10/20 20:27:09 Readed IP: X.Y.Z.A
2021/10/20 20:27:09 Connecting with Cloudflare services...
2021/10/20 20:27:09 cloudflare ddns updater took 101.71621ms
invalid credentials: API Token must not be empty
```

Make sure you have successfully set the `X_CF_AGENT_TOKEN` environment variable with a valid Cloudflare token.

###

```bash
2021/10/20 20:28:56 Updating device IP. Please wait...
2021/10/20 20:28:56 Requesting IP check for: example.com.rpi
2021/10/20 20:28:56 Reading current device IP. Please wait...
2021/10/20 20:28:57 Readed IP: X.Y.Z.A
2021/10/20 20:28:57 Connecting with Cloudflare services...
2021/10/20 20:28:58 cloudflare ddns updater took 1.070368081s

ListZonesContext command failed: HTTP status 400: Invalid request headers (6003)
```
Make sure you have successfully set the `X_CF_AGENT_TOKEN` and the token has the ability to manage your DNS Zone data.
If you set the wrong scope of the token, you have to create a new one.

## License

MIT
26 changes: 26 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package api

import (
"log"
"time"

"github.com/zerjioang/cf-agent/datatypes"
)

// Start executes the IP update method
func Start() error {
defer measureTime(time.Now(), "cloudflare ddns updater")
log.Println("Updating device IP. Please wait...")
p := datatypes.NewPayload()
log.Println("Requesting IP check for: ", p.Zone+"."+p.DNSRecord)
log.Println("Reading current device IP. Please wait...")
currIp := externalIP()
log.Println("Readed IP: ", currIp)
log.Println("Connecting with Cloudflare services...")
return triggerDDNSUpdate(p.Token, p.Zone, "console", currIp)
}

func measureTime(start time.Time, name string) {
elapsed := time.Since(start)
log.Printf("%s took %s", name, elapsed)
}
13 changes: 13 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package api

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestStart(t *testing.T) {
t.Run("example", func(t *testing.T) {
assert.NoError(t, Start())
})
}
56 changes: 56 additions & 0 deletions api/cf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package api

import (
"context"
"log"

"github.com/cloudflare/cloudflare-go"
"github.com/pkg/errors"
)

var (
// Most API calls require a Context
ctx = context.Background()
)

// triggerDDNSUpdate returns CLoudFLare zone identifier for given zone name
func triggerDDNSUpdate(token string, zone string, aName string, currIp string) error {
// Construct a new API object
api, err := cloudflare.NewWithAPIToken(token)
if err != nil {
return err
}
// Fetch the zone ID
// Assuming example.com exists in your Cloudflare account already
zoneId, err := api.ZoneIDByName(zone)
if err != nil {
return err
}
log.Println("Cloudflare ZONE ID: ", zoneId)
rr, err := api.DNSRecords(ctx, zoneId, cloudflare.DNSRecord{
Name: aName + "." + zone,
Type: "A",
})
if err != nil {
return err
}
log.Println("Cloudflare DNS RECORD found: ", len(rr), "records")
if len(rr) == 0 {
return errors.New("Could not get a valid DNS A RECORD for given configuration")
}
dnsa := rr[0]
if currIp != dnsa.Content {
log.Println("Last registered IP found with value : ", dnsa.Content)
log.Println("New device IP value will be set to : ", currIp)
dnsa.Content = currIp
updateErr := api.UpdateDNSRecord(ctx, zoneId, dnsa.ID, dnsa)
if updateErr != nil {
log.Println("failed to update DNS A record due to: ", err)
return err
}
log.Println("Update successful")
return nil
}
log.Println("No need to update DNS A Record")
return nil
}
46 changes: 46 additions & 0 deletions api/ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package api

import (
"encoding/json"
"io/ioutil"
"net"
"net/http"
"strings"
)

// OutboundIP returns external IP of current device
//
// NOTE: Unfortunately, this will only work on networks
// that don't employ the use of NAT.
func OutboundIP() string {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return ""
}
defer conn.Close()
localAddr := conn.LocalAddr().String()
idx := strings.LastIndex(localAddr, ":")
return localAddr[0:idx]
}

// externalIP returns external IP of current device
//
// NOTE: Unfortunately, this will only work on networks
// that don't employ the use of NAT.
func externalIP() string {
req, err := http.Get("http://ip-api.com/json/")
if err != nil {
return err.Error()
}
defer req.Body.Close()
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return err.Error()
}
type IP struct {
Query string
}
var ip IP
_ = json.Unmarshal(body, &ip)
return ip.Query
}
30 changes: 30 additions & 0 deletions api/ip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package api

import (
"log"
"net"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIp(t *testing.T) {
t.Run("external", func(t *testing.T) {
external := externalIP()
log.Println("External IP is", external)
assert.NotNil(t, external)
assert.NotEmpty(t, external)
ip := net.ParseIP(external)
assert.NotNil(t, ip)
assert.NotEmpty(t, ip)
})
t.Run("outbound", func(t *testing.T) {
external := OutboundIP()
log.Println("Outbound IP is", external)
assert.NotNil(t, external)
assert.NotEmpty(t, external)
ip := net.ParseIP(external)
assert.NotNil(t, ip)
assert.NotEmpty(t, ip)
})
}
40 changes: 40 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/zerjioang/cf-agent/api"
)

var rootCmd = &cobra.Command{
Use: "cfagent",
Short: "CloudFlare DDNS Agent",
Long: `CloudFlare DDNS Agent`,
Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Help(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}

var startCmd = &cobra.Command{
Use: "update",
Short: "start service",
Long: `start service`,
Run: func(cmd *cobra.Command, args []string) {
if err := api.Start(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
},
}

func init() {
rootCmd.AddCommand(startCmd)
}
func Execute() error {
return rootCmd.Execute()
}
12 changes: 12 additions & 0 deletions datatypes/cf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package datatypes

// ErrorResponse Cloudflare API error response message format
type ErrorResponse struct {
Success bool `json:"success"`
Errors []struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"errors"`
Messages []interface{} `json:"messages"`
Result interface{} `json:"result"`
}
Loading

0 comments on commit 48e4b21

Please sign in to comment.