├── .gitignore ├── Makefile ├── README.md ├── config.env.example ├── deploy ├── Dockerfile ├── ca-certificates.crt └── k8s │ └── deployment.yaml ├── glide.lock ├── glide.yaml └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | cloudflare-go 3 | cloudflare-dyndns 4 | config.env 5 | manifest-tool -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY?=stevemcquaid 2 | IMAGE?=cloudflare-dyndns 3 | #TEMP_DIR:=$(shell mktemp -d) 4 | TEMP_DIR:=$(shell pwd)/build 5 | ARCH?=amd64 6 | #ALL_ARCH=amd64 arm arm64 ppc64le s390x 7 | ALL_ARCH=amd64 8 | # ML_PLATFORMS=linux/amd64,linux/arm,linux/arm64,linux/ppc64le,linux/s390x 9 | ML_PLATFORMS=linux/amd64 10 | OUT_DIR?=./_output 11 | VENDOR_DOCKERIZED=0 12 | EXEC_NAME=run 13 | 14 | VERSION?=latest 15 | 16 | ifeq ($(ARCH),amd64) 17 | BASEIMAGE?=alpine 18 | endif 19 | ifeq ($(ARCH),arm) 20 | BASEIMAGE?=armhf/busybox 21 | endif 22 | ifeq ($(ARCH),arm64) 23 | BASEIMAGE?=aarch64/busybox 24 | endif 25 | ifeq ($(ARCH),ppc64le) 26 | BASEIMAGE?=ppc64le/busybox 27 | endif 28 | ifeq ($(ARCH),s390x) 29 | BASEIMAGE?=s390x/busybox 30 | endif 31 | 32 | .PHONY: all build docker-build push-% push test verify-gofmt gofmt verify 33 | 34 | all: build 35 | build: vendor 36 | CGO_ENABLED=0 GOARCH=$(ARCH) go build -a -tags netgo -o $(OUT_DIR)/$(ARCH)/$(EXEC_NAME) github.com/$(REGISTRY)/$(IMAGE) 37 | 38 | ## Build docker image 39 | docker-build: vendor 40 | mkdir -p $(TEMP_DIR) 41 | cp deploy/Dockerfile $(TEMP_DIR) 42 | cp deploy/ca-certificates.crt $(TEMP_DIR) 43 | cd $(TEMP_DIR) && sed -i "s|BASEIMAGE|$(BASEIMAGE)|g" Dockerfile 44 | 45 | docker run -it -v $(TEMP_DIR):/build -v $(shell pwd):/go/src/github.com/$(REGISTRY)/$(IMAGE) -e GOARCH=$(ARCH) golang:1.8 /bin/bash -c "\ 46 | CGO_ENABLED=0 go build -a -tags netgo -o /build/$(EXEC_NAME) github.com/$(REGISTRY)/$(IMAGE)" 47 | 48 | docker build -t $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) $(TEMP_DIR) 49 | docker tag $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) $(REGISTRY)/$(IMAGE):$(VERSION) 50 | docker push $(REGISTRY)/$(IMAGE)-$(ARCH):$(VERSION) 51 | #rm -rf $(TEMP_DIR) 52 | 53 | push-%: 54 | $(MAKE) ARCH=$* docker-build 55 | docker push $(REGISTRY)/$(IMAGE)-$*:$(VERSION) 56 | 57 | push: ./manifest-tool $(addprefix push-,$(ALL_ARCH)) 58 | ./manifest-tool push from-args --platforms $(ML_PLATFORMS) --template $(REGISTRY)/$(IMAGE)-ARCH:$(VERSION) --target $(REGISTRY)/$(IMAGE):$(VERSION) 59 | 60 | ./manifest-tool: 61 | curl -sSL https://github.com/estesp/manifest-tool/releases/download/v0.5.0/manifest-tool-linux-amd64 > manifest-tool 62 | chmod +x manifest-tool 63 | 64 | vendor: glide.lock 65 | ifeq ($(VENDOR_DOCKERIZED),1) 66 | docker run -it -v $(shell pwd):/go/src/github.com/$(REGISTRY)/$(IMAGE) -w /go/src/github.com/$(REGISTRY)/$(IMAGE) golang:1.8 /bin/bash -c "\ 67 | curl https://glide.sh/get | sh \ 68 | && glide install -v" 69 | else 70 | glide install -v 71 | endif 72 | 73 | test: vendor 74 | CGO_ENABLED=0 go test ./pkg/... 75 | 76 | verify-gofmt: 77 | ./hack/gofmt-all.sh -v 78 | 79 | gofmt: 80 | ./hack/gofmt-all.sh 81 | 82 | verify: verify-gofmt test 83 | 84 | ## Run docker container locally 85 | docker-run: 86 | docker run -it --env-file config.env $(REGISTRY)/$(IMAGE):latest ./$(EXEC_NAME) 87 | 88 | run: 89 | ./$(OUT_DIR)/$(ARCH)/$(EXEC_NAME) 90 | 91 | ## Deploy to k8s 92 | deploy-k8s: 93 | kubectl apply -f deploy/k8s/deployment.yaml 94 | kubectl create secret generic $(IMAGE) --from-file=config.env 95 | 96 | ## Delete k8s deployment 97 | deploy-k8s-undo: 98 | kubectl delete -f deploy/k8s/deployment.yaml 99 | kubectl delete secret $(IMAGE) 100 | 101 | ## This help message 102 | help: 103 | @printf "Available targets:\n\n" 104 | @awk '/^[a-zA-Z\-\_0-9%:\\]+:/ { \ 105 | helpMessage = match(lastLine, /^## (.*)/); \ 106 | if (helpMessage) { \ 107 | helpCommand = $$1; \ 108 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 109 | gsub("\\\\", "", helpCommand); \ 110 | gsub(":+$$", "", helpCommand); \ 111 | printf " \x1b[32;01m%-35s\x1b[0m %s\n", helpCommand, helpMessage; \ 112 | } \ 113 | } \ 114 | { lastLine = $$0 }' $(MAKEFILE_LIST) | sort -u 115 | @printf "\n" 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare-DynDNS 2 | 3 | Golang implementation of DynDNS on CloudFlare. Update a dns record to be your specific public IP (which might change over time). 4 | 5 | # Usage 6 | 7 | - `cp config.env.example config.env` - Create config.env file 8 | - `nano config.env` - Edit file with your creds + subdomain + domain 9 | - `make docker-build` - Compile the go code + build container 10 | - `make docker-run` - Run docker container with config.env as environment vars in container 11 | 12 | # Todo 13 | - [ ] Implement glog / logging levels 14 | - [ ] Clean up main.go 15 | - [ ] Clean Makefile/scripts 16 | - [X] Create k8s secret generator from config.env file? 17 | - [X] Make secret mounted as volume/file, pass --env-file to main.go 18 | - [ ] Dockerhub auto tag images 19 | -------------------------------------------------------------------------------- /config.env.example: -------------------------------------------------------------------------------- 1 | DOMAIN=example.com 2 | CF_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3 | CF_API_EMAIL=example@gmail.com 4 | SUBDOMAIN=dyndns 5 | 6 | -------------------------------------------------------------------------------- /deploy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | ADD ca-certificates.crt /etc/ssl/certs/ 4 | 5 | COPY run / 6 | 7 | CMD ["/run"] 8 | -------------------------------------------------------------------------------- /deploy/k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: cloudflare-dyndns 5 | labels: 6 | app: cloudflare-dyndns 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: cloudflare-dyndns 12 | template: 13 | metadata: 14 | labels: 15 | app: cloudflare-dyndns 16 | spec: 17 | containers: 18 | - image: stevemcquaid/cloudflare-dyndns-amd64:latest 19 | imagePullPolicy: Always 20 | name: cloudflare-dyndns 21 | args: 22 | - "/run" 23 | - "--config" 24 | - "/config/config.env" 25 | #env: 26 | #- name: DOMAIN 27 | #value: rbaz.com 28 | #- name: SUBDOMAIN 29 | #value: ip 30 | volumeMounts: 31 | - name: cloudflare-dyndns 32 | mountPath: "/config/" 33 | readOnly: true 34 | volumes: 35 | - name: cloudflare-dyndns 36 | secret: 37 | secretName: cloudflare-dyndns 38 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 74583a9988605ee9ad6325f32c287ba278108c11a7c75eba0ed459919f98cf66 2 | updated: 2018-04-26T22:11:12.614588934-04:00 3 | imports: 4 | - name: github.com/cloudflare/cloudflare-go 5 | version: 667a72333f4e362f45844ea90caef0e49ff8f97c 6 | - name: github.com/joho/godotenv 7 | version: 1709ab122c988931ad53508747b3c061400c2984 8 | - name: github.com/pkg/errors 9 | version: a22138067af1c4942683050411a841ade67fe1eb 10 | - name: golang.org/x/net 11 | version: 1c05540f6879653db88113bc4a2b70aec4bd491f 12 | subpackages: 13 | - context 14 | - name: golang.org/x/time 15 | version: f51c12702a4d776e4c1fa9b0fabab841babae631 16 | subpackages: 17 | - rate 18 | testImports: [] 19 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/stevemcquaid/cloudflare-dyndns 2 | import: 3 | - package: github.com/cloudflare/cloudflare-go 4 | 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "errors" 9 | "flag" 10 | "io/ioutil" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/cloudflare/cloudflare-go" 16 | "github.com/joho/godotenv" 17 | ) 18 | 19 | var OLD_IP string 20 | var DOMAIN string 21 | var CF_API_KEY string 22 | var CF_API_EMAIL string 23 | var SUBDOMAIN string 24 | 25 | func argParse() error { 26 | configfile := flag.String("config", "", "Absolute path to the config env file") 27 | flag.Parse() 28 | 29 | if *configfile != "" { 30 | // Load dotenv file into environment, overriding existing vars 31 | err := godotenv.Load(*configfile) 32 | if err != nil { 33 | log.Fatal("Error loading .env file") 34 | } 35 | } 36 | 37 | // Get vars from environment 38 | DOMAIN = os.Getenv("DOMAIN") 39 | if DOMAIN == "" { 40 | msg := fmt.Sprintf("Need to define DOMAIN var") 41 | return errors.New(msg) 42 | } 43 | CF_API_KEY = os.Getenv("CF_API_KEY") 44 | if CF_API_KEY == "" { 45 | msg := fmt.Sprintf("Need to define CF_API_KEY var") 46 | return errors.New(msg) 47 | } 48 | CF_API_EMAIL = os.Getenv("CF_API_EMAIL") 49 | if CF_API_EMAIL == "" { 50 | msg := fmt.Sprintf("Need to define CF_API_EMAIL var") 51 | return errors.New(msg) 52 | } 53 | SUBDOMAIN = os.Getenv("SUBDOMAIN") 54 | if SUBDOMAIN == "" { 55 | msg := fmt.Sprintf("Need to define SUBDOMAIN var") 56 | return errors.New(msg) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func main() { 63 | err := argParse() 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | log.SetOutput(os.Stdout) 69 | 70 | //OLD_IP = getMyIP(4) 71 | //dynDNS(OLD_IP) 72 | checkIP() 73 | 74 | log.Println("Entering Control Loop... ") 75 | for { 76 | time.Sleep(60 * time.Second) 77 | go checkIP() 78 | } 79 | } 80 | 81 | func checkIP() { 82 | log.Printf("Checking IP...\n") 83 | new_ip := getMyIP(4) 84 | if OLD_IP == "" { 85 | // First Run 86 | dynDNS(new_ip) 87 | } else if OLD_IP != new_ip { 88 | log.Printf("IP Address Changed: %s -> %s", OLD_IP, new_ip) 89 | dynDNS(new_ip) 90 | } 91 | OLD_IP = new_ip 92 | } 93 | 94 | func dynDNS(ip string) { 95 | // Construct a new API object 96 | api, err := cloudflare.New(CF_API_KEY, CF_API_EMAIL) 97 | if err != nil { 98 | log.Fatal(err) 99 | 100 | } 101 | 102 | // Fetch the zone ID 103 | zoneID, err := api.ZoneIDByName(DOMAIN) // Assuming example.com exists in your Cloudflare account already 104 | if err != nil { 105 | log.Fatal(err) 106 | return 107 | } 108 | 109 | // Record to create 110 | newRecord := cloudflare.DNSRecord{ 111 | Type: "A", 112 | Name: SUBDOMAIN + "." + DOMAIN, 113 | Content: getMyIP(4), 114 | } 115 | 116 | updateRecord(zoneID, api, &newRecord) 117 | log.Println("Set DNSRecord:", newRecord.Name, newRecord.Content, "\n") 118 | 119 | // Print records 120 | //showCurrentRecords(zoneID, api) 121 | } 122 | 123 | func updateRecord(zoneID string, api *cloudflare.API, newRecord *cloudflare.DNSRecord) { 124 | // Get current records 125 | //log.Println("Getting old dns records... ") 126 | dns := cloudflare.DNSRecord{Type: newRecord.Type, Name: newRecord.Name} 127 | old_records, err := api.DNSRecords(zoneID, dns) 128 | if err != nil { 129 | log.Fatal(err) 130 | return 131 | } 132 | 133 | if len(old_records) == 1 { 134 | // Update 135 | err := api.UpdateDNSRecord(zoneID, old_records[0].ID, *newRecord) 136 | if err != nil { 137 | log.Fatal(err) 138 | return 139 | } 140 | return 141 | } 142 | 143 | if len(old_records) > 1 { 144 | // Delete every record 145 | for _, record := range old_records { 146 | err := api.DeleteDNSRecord(zoneID, record.ID) 147 | if err != nil { 148 | log.Fatal(err) 149 | return 150 | } 151 | msg := fmt.Sprintf("Deleted DNSRecord: %s - %s: %s", record.Type, record.Name, record.Content) 152 | log.Println(msg) 153 | } 154 | } 155 | 156 | // Create if < 1 or > 1 157 | _, err = api.CreateDNSRecord(zoneID, *newRecord) 158 | if err != nil { 159 | log.Fatal(err) 160 | return 161 | } 162 | //log.Println("Done") 163 | } 164 | 165 | func showCurrentRecords(zoneID string, api *cloudflare.API) { 166 | // Fetch all DNS records for example.org 167 | records, err := api.DNSRecords(zoneID, cloudflare.DNSRecord{}) 168 | if err != nil { 169 | log.Println(err) 170 | return 171 | } 172 | 173 | for _, r := range records { 174 | msg := fmt.Sprintf("%s: %s", r.Name, r.Content) 175 | log.Println(msg) 176 | } 177 | } 178 | 179 | func getMyIP(protocol int) string { 180 | var target string 181 | if protocol == 4 { 182 | target = "http://myexternalip.com/raw" 183 | } else { 184 | return "" 185 | 186 | } 187 | resp, err := http.Get(target) 188 | if err == nil { 189 | contents, err := ioutil.ReadAll(resp.Body) 190 | if err == nil { 191 | defer resp.Body.Close() 192 | return strings.TrimSpace(string(contents)) 193 | 194 | } 195 | 196 | } 197 | return "" 198 | } 199 | --------------------------------------------------------------------------------