-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
src/mbdns | ||
src/mbdns.conf | ||
bin/* | ||
dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |