From 7b24cd36ae1aa46c0106faa32198ad32d54f9eea Mon Sep 17 00:00:00 2001 From: Navratan Lal Gupta Date: Sat, 2 Apr 2022 16:33:43 +0530 Subject: [PATCH 1/3] Namecheap Dynamic DNS client --- Dockerfile | 22 +++++++ container-entrypoint.sh | 11 ++++ go.mod | 3 +- go.sum | 20 +++++++ logger.go | 36 ++++++++++++ main.go | 31 +++++++++- model.go | 21 +++++++ updaterecord.go | 125 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100644 container-entrypoint.sh create mode 100644 go.sum create mode 100644 logger.go create mode 100644 model.go create mode 100644 updaterecord.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..76d0fed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.17.7-alpine3.15 as build +ARG OS +ARG ARCH +COPY . /build/ +WORKDIR /build +RUN go mod download && GOOS=${OS} GOARCH=${ARCH} go build -o ncddns + +FROM alpine:3.15 +ARG VERSION +ARG user=ncddns +ARG group=ncddns +ARG uid=1000 +ARG gid=1000 +USER root +WORKDIR /app +COPY --from=build /build/ncddns /app/ncddns +COPY container-entrypoint.sh /app/container-entrypoint.sh +RUN apk update && apk --no-cache add bash && addgroup -g ${gid} ${group} && adduser -h /app -u ${uid} -G ${group} -s /bin/bash -D ${user} +RUN chown ncddns:ncddns /app/ncddns && chmod +x /app/ncddns && \ + chown ncddns:ncddns /app/container-entrypoint.sh && chmod +x /app/container-entrypoint.sh +USER ncddns +ENTRYPOINT [ "/app/container-entrypoint.sh"] \ No newline at end of file diff --git a/container-entrypoint.sh b/container-entrypoint.sh new file mode 100644 index 0000000..a022bdf --- /dev/null +++ b/container-entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -e + +if [ "$NC_HOST" == "" -o "$NC_DOMAIN" == "" -o "$NC_PASS" == "" ]; then + echo "ERROR NC_HOST, NC_DOMAIN and GD_PASS are mandatory." + echo "Use --env with docker run to pass these environment variables." + exit 1 +fi + +/app/ncddns --host="$NC_HOST" --domain="$NC_DOMAIN" --password="$NC_PASS" \ No newline at end of file diff --git a/go.mod b/go.mod index 4b3db0b..6469e1b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,4 @@ -module github.com/navilg/namecheap-ddns +module github.com/navilg/namecheap-ddns-docker go 1.17 + diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..61dd4fd --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jedib0t/go-pretty/v6 v6.3.0 h1:QQ5yZPDUMEjbZRXDJtZlvwfDQqCYFaxV3yEzTkogUgk= +github.com/jedib0t/go-pretty/v6 v6.3.0/go.mod h1:FMkOpgGD3EZ91cW8g/96RfxoV7bdeJyzXPYgz1L1ln0= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c h1:uHnKXcvx6SNkuwC+nrzxkJ+TpPwZOtumbhWrrOYN5YA= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..fe679db --- /dev/null +++ b/logger.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "log" + "os" +) + +const ( + ErrorLog string = "ERROR" + InformationLog string = "INFO" + WarningLog string = "WARN" +) + +func DDNSLogger(logType, host, domain, message string) { + + var ( + StdoutInfoLogger *log.Logger + StdoutWarningLogger *log.Logger + StdoutErrorLogger *log.Logger + ) + + StdoutInfoLogger = log.New(os.Stdout, "INFO ", log.Ldate|log.Ltime) + StdoutWarningLogger = log.New(os.Stdout, "WARN ", log.Ldate|log.Ltime) + StdoutErrorLogger = log.New(os.Stdout, "ERROR ", log.Ldate|log.Ltime) + + if logType == "INFO" { + StdoutInfoLogger.Println(host+"."+domain, message) + } else if logType == "WARN" { + StdoutWarningLogger.Println(host+"."+domain, message) + } else if logType == "ERROR" { + StdoutErrorLogger.Println(host+"."+domain, message) + } else { + fmt.Println(host+"."+domain, message) + } +} diff --git a/main.go b/main.go index f0d9f06..2e329a6 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,34 @@ package main -import "fmt" +import ( + "flag" + "fmt" + "os" +) func main() { - fmt.Println("Hello World!!!") + fmt.Println("Namecheap Dynamic DNS client Version", version) + fmt.Println("Git Repo:", gitrepo) + + domain := flag.String("domain", "", "Domain name e.g. example.com") + host := flag.String("host", "", "Subdomain or hostname e.g. www") + password := flag.String("password", "", "Dynamic DNS Password from Namecheap") + + flag.Parse() + if *domain == "" || *host == "" || *password == "" { + fmt.Println("ERROR domain, host and Dynamic DDNS password are mandatory") + fmt.Printf("\nUsage of %s:\n", os.Args[1]) + flag.PrintDefaults() + os.Exit(1) + } + + pubIp, err := getPubIP() + if err != nil { + DDNSLogger(ErrorLog, *host, *domain, "Failed to get public Ip of your machine. "+err.Error()) + } else { + setDNSRecord(*host, *domain, *password, pubIp) + DDNSLogger(InformationLog, *host, *domain, "Record updated.") + } + + updateRecord(*domain, *host, *password) } diff --git a/model.go b/model.go new file mode 100644 index 0000000..08c6a68 --- /dev/null +++ b/model.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "time" +) + +type CustomError struct { + ErrorCode int + Err error +} + +func (err *CustomError) Error() string { + return fmt.Sprintf("Error: %v, StatusCode: %d", err.Err, err.ErrorCode) +} + +var ( + version string = "1.0.0-go1.17" + daemon_poll_time time.Duration = 1 * time.Minute // Time in minute + gitrepo string = "https://github.com/navilg/namecheap-ddns-docker" +) diff --git a/updaterecord.go b/updaterecord.go new file mode 100644 index 0000000..15ffd97 --- /dev/null +++ b/updaterecord.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "os" + "os/signal" + "time" +) + +func updateRecord(domain, host, password string) { + + DDNSLogger(InformationLog, "", "", "Started daemon process") + + ticker := time.NewTicker(daemon_poll_time) + done := make(chan bool) + + go func() { + for { + select { + case <-done: + return + + case <-ticker.C: + pubIp, err := getPubIP() + if err != nil { + DDNSLogger(ErrorLog, host, domain, err.Error()) + } + + err = setDNSRecord(host, domain, password, pubIp) + if err != nil { + DDNSLogger(ErrorLog, host, domain, err.Error()) + } + + DDNSLogger(InformationLog, host, domain, "Record updated (ip: "+os.Getenv("NC_PUB_IP")+"->"+pubIp+")") + } + } + + }() + + // Handle signal interrupt + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + DDNSLogger(InformationLog, "", "", "Interrupt signal received. Exiting") + ticker.Stop() + done <- true + os.Exit(0) + } + }() + + time.Sleep(8760 * time.Hour) // Sleep for 365 days + ticker.Stop() + done <- true +} + +func getPubIP() (string, error) { + + type GetIPBody struct { + IP string `json:"ip"` + } + + var ipbody GetIPBody + + response, err := http.Get("https://ipinfo.io/json") + if err != nil { + return "", nil + } + + defer response.Body.Close() + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + // fmt.Println(err.Error()) + return "", err + } + + err = json.Unmarshal(bodyBytes, &ipbody) + if err != nil { + // fmt.Println(err.Error()) + return "", err + } + + return ipbody.IP, nil + +} + +func setDNSRecord(host, domain, password, pubIp string) error { + + // Link from Namecheap knowledge article. + // https://www.namecheap.com/support/knowledgebase/article.aspx/29/11/how-to-dynamically-update-the-hosts-ip-with-an-http-request/ + + ncURL := "https://dynamicdns.park-your-domain.com/update?host=" + host + "&domain=" + domain + "&password=" + password + "&ip=" + pubIp + + apiclient := &http.Client{} + + req, err := http.NewRequest("GET", ncURL, nil) + if err != nil { + // fmt.Println(1, err.Error()) + return err + } + + // req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*") + // req.Header.Add("Accept-Encoding", "gzip, deflate, br") + // req.Header.Add("Connection", "keep-alive") + + response, err := apiclient.Do(req) + if err != nil { + // fmt.Println(2, err.Error()) + return err + } + + defer response.Body.Close() + + if response.StatusCode != 200 { + return &CustomError{ErrorCode: response.StatusCode, Err: errors.New(response.Status)} + } + + os.Setenv("NC_PUB_IP", pubIp) + + return nil +} From fc2228c6fa9131013751743f6ae2d8a03ba29cb9 Mon Sep 17 00:00:00 2001 From: Navratan Lal Gupta Date: Sat, 2 Apr 2022 16:51:57 +0530 Subject: [PATCH 2/3] Update README.md --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 386f8e9..50a7fa6 100644 --- a/README.md +++ b/README.md @@ -1 +1,58 @@ -# namecheap-ddns \ No newline at end of file +# Namecheap DDNS docker client + +## When to use ? + +If your server do not have static IP, i.e. When Public IP of your server / router keep changing, This will automatically update new IP to Namecheap record. + +## Why to use it ? + +* Easy to setup + +* Lightweight + +* No cronjob configuration required + +* Logs for visibility + +* Open source + +## How to use it + +* Enable Dynamic DNS for your domain from Namecheap. `Namecheap Account -> Domain List -> Manage -> Advanced DNS -> Dynamic DNS -> Toggle Status` + +* Copy the Dynamic DNS password which is generated after enabling Dynamic DNS. Keep it safe and handy. + +* Add a record of type `A + Dynamic DNS` with required host name. + +* Install docker on server. + +* Run below command + +Suppose, You need DDNS for `server.example.com` + +``` +docker run --name server.example.com -d --restart unless-stopped -e NC_HOST='server' -e NC_DOMAIN='example.com' -e NC_PASS='DynamicDDNSPa2w0rd' linuxshots/namecheap-ddns:1.0.0 +``` + +Here, +`NC_HOST` is host name added in Namecheap record. + +`NC_DOMAIN` is your domain name. + +`NC_PASS` is your Dynamic DDNS password which is generated from Namecheap. + +* Check the log + +``` +docker logs server.example.com +``` + +* To stop, start and remove DDNS. + +``` +docker stop server.example.com # To stop +docker start server.example.com # To start after its stopped +docker rm server.example.com -f # To remove +``` + +NOTE: This sets the TTL to Automatic i.e. 30 minutes. Currently, There is no way provided by Namecheap to set custom TTL in Dynamic DDNS. \ No newline at end of file From 8eb70eb2e18a0ad78cb8c955d160ab7966682b76 Mon Sep 17 00:00:00 2001 From: Navratan Lal Gupta Date: Sat, 2 Apr 2022 16:55:27 +0530 Subject: [PATCH 3/3] Add build your own image --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 50a7fa6..d3eb175 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,17 @@ docker start server.example.com # To start after its stopped docker rm server.example.com -f # To remove ``` +## Build your own image + +To build your own image + +* Clone this repo + +* Run docker build + +``` +# Replace OS and ARCH values with valid values of GOlang environment variables GOOS and GOARCH. +docker build --build-arg OS=linux --build-arg ARCH=amd64 --build-arg VERSION=1.0.0 -t linuxshots/namecheap-ddns:1.0.0 . +``` + NOTE: This sets the TTL to Automatic i.e. 30 minutes. Currently, There is no way provided by Namecheap to set custom TTL in Dynamic DDNS. \ No newline at end of file