Skip to content

Commit

Permalink
1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rys committed Sep 9, 2018
1 parent 68bb0a5 commit 931b67b
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
src/mbdns
src/mbdns.conf
bin/*
dist/*
17 changes: 17 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 08-09-2018

Functioning implementation tested on `darwin-amd64`, `freebsd-amd64` and `linux-mipsle`

+ [x] `darwin-amd64` (macOS 10.13)
+ [x] `freebsd-amd64` (FreeNAS 11)
+ [x] `linux-mipsle` (EdgeOS 1.10, ER-X)
+ [x] Runs from secured JSON-based config
+ [x] Supports `A` and `AAAA` record types against the correct API endpoint
+ [x] Error diagnosis on non-200 returns
+ [x] 1s delay between record update attempts (compile time)
+ [x] 300s delay between full loop update attempts (compile time)
+ [x] Scripts for building a release with embedded `BuildVersion`, `BuildDate` and `GitRev` (printed to console at start)

# 09-09-2018

+ [x] Support relocating and renaming `./mbdns.conf` with `--config`
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contributing to mbdns

We welcome contributions to `mbdns` of any kind, including documentation, functional patches, bug reports, issues, feature requests, feature implementations, pull requests, etc.

## Building mbdns

Run [src/build-test](/src/build-test) to build a single binary from your git checkout. `go build` will place it into `../bin/mbdns`.

Run [src/build-rel RELEASE_NAME](/src/build-rel) if you'd like to build all supports binaries for a release. The release script will put the current git revision for `HEAD` into `BuildVersion` if you don't supply `RELEASE_NAME`. `go build` will place binaries into `../bin/RELEASE_NAME`.

## Current supported platform configs

While there's no golang code in `mbdns` that isn't portable to any platform that golang supports, the release build script only builds for platforms we've tested and are known to work.

Those configs are:

| GOOS | GOARCH | Hardware and OS platform(s) |
| :-----: | :----: | :---------------------------------- |
| linux | arm | QNAP |
| linux | mipsle | Ubiquiti EdgeRouter X, EdgeOS v1.10 |
| darwin | amd64 | macOS 10.13, iMac 5K |
| freebsd | amd64 | freenas/11.1-stable, HP gen8 G1610T |
| linux | amd64 | Ubuntu 18.04 LTS |

## Golang requirements

`mbdns` has no special golang requirements. Releases are currently generated by `go version go1.11 darwin/amd64`.

## Licensing

`mbdns` is [MIT licensed](/LICENSE).
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# mbdns

`mbdns` is a dynamic DNS update client for the [Mythic Beasts](https://www.mythic-beasts.com/support/api/primary) Primary DNS system, supporting their IPv4 and IPv6 endpoints. Written in [golang](https://golang.org), it supports common home network infrastructure operating systems (FreeBSD, EdgeOS, Linux) and common platform architectures (amd64 and MIPS).

## Configuring mbdns

Take [doc/mbdns.conf.sample](/doc/mbdns.conf.sample) and configure it to your needs, saving as `mbdns.conf`. `mbdns.conf` must be valid JSON or `mbdns` will fail to start.

## Deploying mbdns

* Copy the `mbdns` binary (usually named `mbdns-VERSION-OS-ARCH`) and `mbdns.conf` to your target platform
* chmod 0400 mbdns.conf so the Mythic Beasts API tokens can only be read by the user you deploy as (ideally use a specific user). `mbdns` will check for you and fail to start if the config is insecurely readable.
* Run the `mbdns` binary. It will run until you kill it.
* By default the `mbdns` binary will look for `./mbdns.conf`. You can relocate it (and rename it) and tell `mbdns` with `--config`.
* `mbdns --config /etc/mbdns/mbdns.conf`

`mbdns` is written in golang and builds as a statically linked library with no dependencies.

## Practical running

`mbdns` logs to `stdout`. If your target OS supports it, redirect stdout to a file, logrotate that file, and run the binary in the background via the daemonising system of your choice. `mbdns` makes no attempt to daemonise itself.

`mbdns --version` prints the version information and exits immediately.

Running on a Unix-like might go something like this (assuming `mbdns` is in `$PATH`):

`nohup mbdns --config /etc/mbdns/mbdns.conf > /var/log/mbdns 2>&1 &`

## Logging

`mbdns` logs the following:

* version, build date and git commit SHA1 on startup
* the path to the config file
* the update command it is attempting to run for the host and domain
* success or failure including record type and TTL in both cases
* a small handful of at-startup failure messages if it can't run (no conf, invalid JSON, insecure conf)
* a message saying it is processing records if it can cleanly start

## License

`mbdns` is MIT licensed
58 changes: 58 additions & 0 deletions doc/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Basic architecture of mbdns

- [x] Read JSON-encoded `[domain token host ttl]` record tuples from a (checked) `0400` file called `mbdns.conf` that lives next to the binary
- [x] Iterate over each tuple and:
- [x] https `POST` `"domain=t.domain;password=t.token;command=REPLACE t.host t.ttl A DYNAMIC_IP"` to `dnsapi.mythic-beasts.com`
- [x] Record success and failure but don't handle failure. Just try again later next time around the loop.
- [x] Do that in loop with a compile-time loop sleep in a background thread/goroutine, running daemonised
- [x] Some basic logging to stdout

# JSON file format

Because of the way golang does magic unmarshalling into structs, our golang code self-documents the JSON format.

```go
type record struct {
Domain string
Token string
Host string
TTL string
Record string
}
```

Is unmarshalled from:

```json
[
{
"domain" : "some_domain",
"token" : "mythic_beasts_api_token",
"host" : "hostname",
"ttl" : "3600",
"record" : "A"
}
]
```

# Communicating with the Mythic Beasts Primary DNS API

Documentation: [here](https://www.mythic-beasts.com/support/api/primary)

API URL: `https://dnsapi.mythic-beasts.com/`

`GET` is supported but we'll use `POST`. `POST` needs an `application/x-www-form-urlencoded`, which we get from [`net/http`](https://golang.org/pkg/net/http/#pkg-overview)'s `PostForm()` API.

Building the payload is easy:

`http.PostForm(mythicbeastsUrl, url.Values{"key": {"value"}, "key2": {"value2"}} ...)`

So we'll do:

`http.PostForm(mbUrl, url.Values{"domain": {record.Domain}, "password": {record.Token}, "command": {builtCommand}})`

We use the `REPLACE` command and build the rest of the `builtCommand` payload with a `Sprintf()` formatted const string.

`REPLACE` takes the form `REPLACE host TTL record DYNAMIC_IP`

We get HTTP response code 200 (OK) back on success, 4xx in the event of a failure.
16 changes: 16 additions & 0 deletions doc/mbdns.conf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"domain" : "yourdomain.com",
"token" : "domain_api_password",
"host" : "hostname",
"ttl" : "3600",
"record": "A"
},
{
"domain" : "yourdomain.com",
"token" : "domain_api_password",
"host" : "hostname",
"ttl" : "3600",
"record": "AAAA"
}
]
32 changes: 32 additions & 0 deletions src/build-rel
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/local/bin/zsh

GIT_SHA=$(git rev-parse --short HEAD)
B=../bin
D=../dist

R=${1}
[[ -z ${R} ]] && R=${GIT_SHA}

[[ -d ${B}/${R} ]] && rm -rf ${B}/${R}
mkdir -p ${B}/${R}
[[ -d ${D}/${R} ]] && rm -rf ${D}/${R}
mkdir -p ${D}/${R}

LDFLAGS="-X main.BuildVersion=${R} -X main.BuildDate=`date -u '+%Y%m%d'` -X main.GitRev=${GIT_SHA}"

GOOS=linux GOARCH=arm go build -ldflags ${LDFLAGS} -o ${B}/${R}/mbdns-${R}-linux-arm mbdns.go
GOOS=linux GOARCH=mipsle go build -ldflags ${LDFLAGS} -o ${B}/${R}/mbdns-${R}-linux-mipsle mbdns.go
GOOS=darwin GOARCH=amd64 go build -ldflags ${LDFLAGS} -o ${B}/${R}/mbdns-${R}-darwin-amd64 mbdns.go
GOOS=freebsd GOARCH=amd64 go build -ldflags ${LDFLAGS} -o ${B}/${R}/mbdns-${R}-freebsd-amd64 mbdns.go
GOOS=linux GOARCH=amd64 go build -ldflags ${LDFLAGS} -o ${B}/${R}/mbdns-${R}-linux-amd64 mbdns.go

for i (linux-arm linux-mipsle darwin-amd64 freebsd-amd64 linux-amd64) do
P=mbdns-${R}-${i}
mkdir -p ${D}/${R}/${P}
cp ../doc/mbdns.conf.sample ${D}/${R}/${P}
cp ../README.md ${D}/${R}/${P}
cp ../LICENSE ${D}/${R}/${P}
cp ${B}/${R}/${P} ${D}/${R}/${P}
tar czf ${D}/${R}/${P}.tar.gz -C ${D}/${R} ${P}
rm -rf ${D}/${R}/${P}
done
8 changes: 8 additions & 0 deletions src/build-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/local/bin/zsh

GIT_SHA=$(git rev-parse --short HEAD)
R=${GIT_SHA}

LDFLAGS="-X main.BuildVersion=${R} -X main.BuildDate=`date -u '+%Y%m%d'` -X main.GitRev=${GIT_SHA}"

go build -ldflags ${LDFLAGS} -o ../bin/mbdns mbdns.go
146 changes: 146 additions & 0 deletions src/mbdns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"time"
)

type record struct {
Domain string
Token string
Host string
TTL string
Record string
}

const mbConfigFile string = "mbdns.conf"
const mbURLv4 string = "https://dnsapi4.mythic-beasts.com/"
const mbURLv6 string = "https://dnsapi6.mythic-beasts.com/"
const mbCommand string = "REPLACE %s %s %s DYNAMIC_IP"
const mbLoopWaitSeconds string = "300s"
const mbRecordUpdateWaitSeconds string = "1s"
const mbResponseError string = "updating %s.%s (%s, %s) failed with %s"
const mbResponseSuccess string = "updating %s.%s (%s, %s) succeeded"
const mbLogActivity string = "running %s for %s.%s"
const mbStartupBanner string = "mbdns %s %s (git %s)"
const mbConfigPathBanner string = "config path: %s"
const mbRecordA = "A"
const mbRecordAAAA = "AAAA"

var records []record

// BuildVersion passed in via ldflags
var BuildVersion string

// BuildDate passed in via ldflags
var BuildDate string

// GitRev passed in via ldflags
var GitRev string

func process() {
loopSleepDuration, _ := time.ParseDuration(mbLoopWaitSeconds)
recordSleepDuration, _ := time.ParseDuration(mbRecordUpdateWaitSeconds)

for {
for i := range records {
command := fmt.Sprintf(mbCommand, records[i].Host, records[i].TTL, records[i].Record)
logActivityMsg := fmt.Sprintf(mbLogActivity, command, records[i].Host, records[i].Domain)

log.Println(logActivityMsg)

mbURL := mbURLv4

if records[i].Record != mbRecordA && records[i].Record != mbRecordAAAA {
continue
}

if records[i].Record == mbRecordAAAA {
mbURL = mbURLv6
}

response, err := http.PostForm(mbURL, url.Values{"domain": {records[i].Domain}, "password": {records[i].Token}, "command": {command}})

if err != nil {
log.Println(fmt.Sprintf(mbResponseError, records[i].Host, records[i].Domain, records[i].Record, records[i].TTL, err.Error()))
continue
}

defer response.Body.Close()

if response.StatusCode != 200 {
log.Println(fmt.Sprintf(mbResponseError, records[i].Host, records[i].Domain, records[i].Record, records[i].TTL, response.Status))

body, _ := ioutil.ReadAll(response.Body)
log.Printf("%s", body)

continue
}

log.Println(fmt.Sprintf(mbResponseSuccess, records[i].Host, records[i].Domain, records[i].Record, records[i].TTL))

time.Sleep(recordSleepDuration)
}

time.Sleep(loopSleepDuration)
}
}

func main() {
log.SetOutput(os.Stdout)

var configFile string
var printVer bool
flag.StringVar(&configFile, "config", mbConfigFile, "Config file path")
flag.BoolVar(&printVer, "version", false, "Print version banner and exit")
flag.Parse()

if printVer {
fmt.Println(fmt.Sprintf(mbStartupBanner, BuildVersion, BuildDate, GitRev))
os.Exit(0)
}

log.Println(fmt.Sprintf(mbStartupBanner, BuildVersion, BuildDate, GitRev))

log.Println(fmt.Sprintf(mbConfigPathBanner, configFile))

if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Fatal("config does not exist. Exiting...")
}

f, err := os.Lstat(configFile)

if err != nil {
log.Fatal("could not stat config. Exiting...")
}

if f.Mode() != 0400 {
log.Fatal("config is potentially insecure. Exiting...")
}

log.Println("mbdns reading config")

tuples, err := ioutil.ReadFile(configFile)

if err != nil {
log.Fatal("could not read config records. Exiting...")
}

err = json.Unmarshal(tuples, &records)

if err != nil {
log.Fatal("could not process config. Invalid JSON? Exiting...")
}

log.Println("mbdns is processing records")

go process()
select {}
}

0 comments on commit 931b67b

Please sign in to comment.