Skip to content

Commit

Permalink
Merge pull request #1 from navilg/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
navilg authored Apr 2, 2022
2 parents 490c591 + 8eb70eb commit 32be9f5
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 4 deletions.
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
# namecheap-ddns
# 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
```

## 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.
11 changes: 11 additions & 0 deletions container-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module github.com/navilg/namecheap-ddns
module github.com/navilg/namecheap-ddns-docker

go 1.17

20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
36 changes: 36 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
31 changes: 29 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -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)
}
21 changes: 21 additions & 0 deletions model.go
Original file line number Diff line number Diff line change
@@ -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"
)
125 changes: 125 additions & 0 deletions updaterecord.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 32be9f5

Please sign in to comment.