You've already forked dyndns-client
							
							Initial Commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | bin | ||||||
							
								
								
									
										14
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | kind: pipeline | ||||||
|  | type: docker | ||||||
|  | name: amd64 | ||||||
|  |  | ||||||
|  | steps: | ||||||
|  | - name: build-linux-amd64 | ||||||
|  |   image: docker.io/volkerraschek/build-image:latest | ||||||
|  |   commands: | ||||||
|  |   - make | ||||||
|  |   when: | ||||||
|  |     event: | ||||||
|  |     - push | ||||||
|  |     - pull_request | ||||||
|  |     - tag | ||||||
							
								
								
									
										12
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | root = true | ||||||
|  |  | ||||||
|  | [*] | ||||||
|  | indent_style = space | ||||||
|  | indent_size = 2 | ||||||
|  | end_of_line = lf | ||||||
|  | charset = utf-8 | ||||||
|  | trim_trailing_whitespace = true | ||||||
|  | insert_final_newline = false | ||||||
|  |  | ||||||
|  | [Makefile] | ||||||
|  | indent_style = tab | ||||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | **/Makefile eol=lf | ||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | bin | ||||||
|  |  | ||||||
|  | pkg/config/*.json | ||||||
|  | !pkg/config/config.json | ||||||
|  |  | ||||||
|  | dyndns-client | ||||||
|  | dyndns-client.rpm | ||||||
							
								
								
									
										27
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | ARG BASE_IMAGE | ||||||
|  | ARG BUILD_IMAGE | ||||||
|  |  | ||||||
|  | # BUILD | ||||||
|  | # =========================================== | ||||||
|  | FROM ${BUILD_IMAGE} AS build | ||||||
|  | ADD . /workspace | ||||||
|  |  | ||||||
|  | ARG EXECUTABLE | ||||||
|  | ARG GONOPROXY | ||||||
|  | ARG GONOSUMDB | ||||||
|  | ARG GOPRIVATE | ||||||
|  | ARG GOPROXY | ||||||
|  | ARG GOSUMDB | ||||||
|  | ARG VERSION | ||||||
|  |  | ||||||
|  | RUN make bin/linux/amd64/${EXECUTABLE} | ||||||
|  |  | ||||||
|  | # TARGET CONTAINER | ||||||
|  | # =========================================== | ||||||
|  | FROM ${BASE_IMAGE} | ||||||
|  |  | ||||||
|  | ARG EXECUTABLE | ||||||
|  |  | ||||||
|  | RUN apk add --update bind-tools | ||||||
|  | COPY --from=build /workspace/bin/linux/amd64/${EXECUTABLE} /usr/bin/${EXECUTABLE} | ||||||
|  | ENTRYPOINT [ "/usr/bin/dyndns-client" ] | ||||||
							
								
								
									
										13
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | Copyright 2021 Markus Pesch | ||||||
|  |  | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  |  | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
							
								
								
									
										146
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | # VERSION | ||||||
|  | VERSION ?= $(shell git describe --abbrev=0)+hash.$(shell git rev-parse --short HEAD) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DESTDIR    ?= | ||||||
|  | PREFIX     ?= /usr/local | ||||||
|  | EXECUTABLE := dyndns-client | ||||||
|  |  | ||||||
|  | # CONTAINER_RUNTIME | ||||||
|  | CONTAINER_RUNTIME ?= $(shell which docker) | ||||||
|  |  | ||||||
|  | # BUILD_IMAGE | ||||||
|  | BUILD_IMAGE_REGISTRY_HOST   := docker.io | ||||||
|  | BUILD_IMAGE_NAMESPACE       := volkerraschek | ||||||
|  | BUILD_IMAGE_REPOSITORY      := build-image | ||||||
|  | BUILD_IMAGE_VERSION         := latest | ||||||
|  | BUILD_IMAGE_FULLY_QUALIFIED := ${BUILD_IMAGE_REGISTRY_HOST}/${BUILD_IMAGE_NAMESPACE}/${BUILD_IMAGE_REPOSITORY}:${BUILD_IMAGE_VERSION:v%=%} | ||||||
|  |  | ||||||
|  | # BASE_IMAGE | ||||||
|  | BASE_IMAGE_REGISTRY_HOST    := docker.io | ||||||
|  | BASE_IMAGE_NAMESPACE        := library | ||||||
|  | BASE_IMAGE_REPOSITORY       := alpine | ||||||
|  | BASE_IMAGE_VERSION          := 3.12.0 | ||||||
|  | BASE_IMAGE_FULLY_QUALIFIED  := ${BASE_IMAGE_REGISTRY_HOST}/${BASE_IMAGE_NAMESPACE}/${BASE_IMAGE_REPOSITORY}:${BASE_IMAGE_VERSION:v%=%} | ||||||
|  |  | ||||||
|  | # CONTAINER_IMAGE | ||||||
|  | CONTAINER_IMAGE_REGISTRY_HOST   := docker.io | ||||||
|  | CONTAINER_IMAGE_NAMESPACE       := volkerraschek | ||||||
|  | CONTAINER_IMAGE_REPOSITORY      := ${EXECUTABLE} | ||||||
|  | CONTAINER_IMAGE_VERSION         := latest | ||||||
|  | CONTAINER_IMAGE_FULLY_QUALIFIED := ${CONTAINER_IMAGE_REGISTRY_HOST}/${CONTAINER_IMAGE_NAMESPACE}/${CONTAINER_IMAGE_REPOSITORY}:${CONTAINER_IMAGE_VERSION:v%=%} | ||||||
|  | CONTAINER_IMAGE_UNQUALIFIED     := ${CONTAINER_IMAGE_NAMESPACE}/${CONTAINER_IMAGE_REPOSITORY}:${CONTAINER_IMAGE_VERSION:v%=%} | ||||||
|  |  | ||||||
|  | # BINARIES | ||||||
|  | # ============================================================================== | ||||||
|  | ${EXECUTABLE}: clean bin/tmp/${EXECUTABLE} | ||||||
|  |  | ||||||
|  | bin/linux/amd64/$(EXECUTABLE): | ||||||
|  | 	CGO_ENABLED=0 \ | ||||||
|  | 	GONOPROXY=$(shell go env GONOPROXY) \ | ||||||
|  | 	GONOSUMDB=$(shell go env GONOSUMDB) \ | ||||||
|  | 	GOPRIVATE=$(shell go env GOPRIVATE) \ | ||||||
|  | 	GOPROXY=$(shell go env GOPROXY) \ | ||||||
|  | 	GOSUMDB=$(shell go env GOSUMDB) \ | ||||||
|  | 	GOOS=linux \ | ||||||
|  | 	GOARCH=amd64 \ | ||||||
|  | 		go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@} | ||||||
|  |  | ||||||
|  | bin/tmp/${EXECUTABLE}: | ||||||
|  | 	CGO_ENABLED=0 \ | ||||||
|  | 	GONOPROXY=$(shell go env GONOPROXY) \ | ||||||
|  | 	GONOSUMDB=$(shell go env GONOSUMDB) \ | ||||||
|  | 	GOPRIVATE=$(shell go env GOPRIVATE) \ | ||||||
|  | 	GOPROXY=$(shell go env GOPROXY) \ | ||||||
|  | 	GOSUMDB=$(shell go env GOSUMDB) \ | ||||||
|  | 		go build -ldflags "-X main.version=${VERSION:v%=%}" -o ${@} | ||||||
|  |  | ||||||
|  | # TEST | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=test | ||||||
|  | test: clean bin/tmp/${EXECUTABLE} | ||||||
|  | 	go test -v ./pkg/... | ||||||
|  |  | ||||||
|  | # CLEAN | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=clean | ||||||
|  | clean: | ||||||
|  | 	rm --force ${EXECUTABLE} || true | ||||||
|  | 	rm --force --recursive bin || true | ||||||
|  |  | ||||||
|  | # CONTAINER IMAGE | ||||||
|  | # ============================================================================== | ||||||
|  | container-image/build: | ||||||
|  | 	${CONTAINER_RUNTIME} build \ | ||||||
|  | 		--build-arg BASE_IMAGE=${BASE_IMAGE_FULLY_QUALIFIED} \ | ||||||
|  | 		--build-arg BUILD_IMAGE=${BUILD_IMAGE_FULLY_QUALIFIED} \ | ||||||
|  | 		--build-arg EXECUTABLE=${EXECUTABLE} \ | ||||||
|  | 		--build-arg GONOPROXY=$(shell go env GONOPROXY) \ | ||||||
|  | 		--build-arg GONOSUMDB=$(shell go env GONOSUMDB) \ | ||||||
|  | 		--build-arg GOPRIVATE=$(shell go env GOPRIVATE) \ | ||||||
|  | 		--build-arg GOPROXY=$(shell go env GOPROXY) \ | ||||||
|  | 		--build-arg GOSUMDB=$(shell go env GOSUMDB) \ | ||||||
|  | 		--build-arg VERSION=${VERSION:v%=%} \ | ||||||
|  | 		--no-cache \ | ||||||
|  | 		--tag ${CONTAINER_IMAGE_FULLY_QUALIFIED} \ | ||||||
|  | 		--tag ${CONTAINER_IMAGE_UNQUALIFIED} \ | ||||||
|  | 		. | ||||||
|  |  | ||||||
|  | container-image/push: container-image/build | ||||||
|  | 	${CONTAINER_RUNTIME} push ${CONTAINER_IMAGE_FULLY_QUALIFIED} | ||||||
|  |  | ||||||
|  | # CONTAINER RUN - TEST | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=container-run/test | ||||||
|  | container-run/test: | ||||||
|  | 	$(MAKE) container-run COMMAND=${@:container-run/%=%} | ||||||
|  |  | ||||||
|  | # CONTAINER RUN - CLEAN | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=container-run/clean | ||||||
|  | container-run/clean: | ||||||
|  | 	$(MAKE) container-run COMMAND=${@:container-run/%=%} | ||||||
|  |  | ||||||
|  | # CONTAINER RUN - COMMAND | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=container-run | ||||||
|  | container-run: | ||||||
|  | 	${CONTAINER_RUNTIME} run \ | ||||||
|  | 		--env GONOPROXY=$(shell go env GONOPROXY) \ | ||||||
|  | 		--env GONOSUMDB=$(shell go env GONOSUMDB) \ | ||||||
|  | 		--env GOPRIVATE=$(shell go env GOPRIVATE) \ | ||||||
|  | 		--env GOPROXY=$(shell go env GOPROXY) \ | ||||||
|  | 		--env GOSUMDB=$(shell go env GOSUMDB) \ | ||||||
|  | 		--env EPOCH=${EPOCH} \ | ||||||
|  | 		--env VERSION=${VERSION:v%=%} \ | ||||||
|  | 		--env RELEASE=${RELEASE} \ | ||||||
|  | 		--rm \ | ||||||
|  | 		--volume $(shell pwd):/workspace \ | ||||||
|  | 			${BUILD_IMAGE_FULLY_QUALIFIED} \ | ||||||
|  | 				make ${COMMAND} \ | ||||||
|  |  | ||||||
|  | # UN/INSTALL | ||||||
|  | # ============================================================================== | ||||||
|  | PHONY+=install | ||||||
|  | install: bin/tmp/${EXECUTABLE} | ||||||
|  | 	install --directory ${DESTDIR}${PREFIX}/bin | ||||||
|  | 	install --mode 755 bin/tmp/${EXECUTABLE} ${DESTDIR}${PREFIX}/bin/${EXECUTABLE} | ||||||
|  |  | ||||||
|  | 	install --directory ${DESTDIR}/usr/lib/systemd/system | ||||||
|  | 	install --mode 644 systemd/${EXECUTABLE}.service ${DESTDIR}/usr/lib/systemd/system | ||||||
|  | 	install --mode 644 systemd/${EXECUTABLE}-docker.service ${DESTDIR}/usr/lib/systemd/system | ||||||
|  |  | ||||||
|  | 	install --directory ${DESTDIR}/usr/share/licenses/${EXECUTABLE} | ||||||
|  | 	install --mode 644 LICENSE ${DESTDIR}/usr/share/licenses/${EXECUTABLE}/LICENSE | ||||||
|  |  | ||||||
|  | PHONY+=uninstall | ||||||
|  | uninstall: | ||||||
|  | 	-rm --recursive --force \ | ||||||
|  | 		${DESTDIR}${PREFIX}/bin/${EXECUTABLE} \ | ||||||
|  | 		${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}.service \ | ||||||
|  | 		${DESTDIR}/usr/lib/systemd/system/${EXECUTABLE}-docker.service \ | ||||||
|  | 		${DESTDIR}/usr/share/licenses/${EXECUTABLE}/LICENSE | ||||||
|  |  | ||||||
|  | # PHONY | ||||||
|  | # ============================================================================== | ||||||
|  | .PHONY: ${PHONY} | ||||||
							
								
								
									
										43
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | # dyndns-client | ||||||
|  |  | ||||||
|  | [](https://drone.cryptic.systems/dyndns-client/dyndns-client) | ||||||
|  |  | ||||||
|  | dyndns-client is a Daemon to listen on interface notifications produced by the linux | ||||||
|  | kernel of a client machine to update one or more DNS zones. | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | To start dyndns-client just run `./dyndns-client`. | ||||||
|  |  | ||||||
|  | ## Configuration | ||||||
|  |  | ||||||
|  | The program is compiled as standalone binary without third party libraries. If | ||||||
|  | no configuration file available under `/etc/dyndns-client/config.json`, than | ||||||
|  | will be the burned in configuration used. If also no configuration be burned | ||||||
|  | into the source code, that the client returned an error. | ||||||
|  |  | ||||||
|  | The example below describes a configuration to update RRecords triggerd by the | ||||||
|  | interface `br0` for the `example.com` zone. To update the zone is a TSIG-Key | ||||||
|  | required. | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "interfaces": [ | ||||||
|  |     "br0" | ||||||
|  |   ], | ||||||
|  |   "zones": { | ||||||
|  |     "example.com": { | ||||||
|  |       "dns-server": "10.6.231.5", | ||||||
|  |       "name": "example.com", | ||||||
|  |       "tsig-key": "my-key" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "tsig-keys": { | ||||||
|  |     "my-key": { | ||||||
|  |       "algorithm": "hmac-sha512", | ||||||
|  |       "name":      "my-key", | ||||||
|  |       "secret":    "asdasdasdasdjkhjk38hcn38haoü2390dndaskdTTWA==" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | module git.cryptic.systems/volker.raschek/dyndns-client | ||||||
|  |  | ||||||
|  | go 1.16 | ||||||
|  |  | ||||||
|  | require ( | ||||||
|  | 	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d | ||||||
|  | 	github.com/sirupsen/logrus v1.8.1 | ||||||
|  | 	github.com/stretchr/testify v1.6.1 // indirect | ||||||
|  | 	github.com/vishvananda/netlink v1.1.0 | ||||||
|  | 	github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe // indirect | ||||||
|  | 	golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect | ||||||
|  | ) | ||||||
							
								
								
									
										26
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= | ||||||
|  | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= | ||||||
|  | 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
|  | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
|  | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= | ||||||
|  | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= | ||||||
|  | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
|  | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
|  | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= | ||||||
|  | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
|  | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= | ||||||
|  | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= | ||||||
|  | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= | ||||||
|  | github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe h1:mjAZxE1nh8yvuwhGHpdDqdhtNu2dgbpk93TwoXuk5so= | ||||||
|  | github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= | ||||||
|  | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= | ||||||
|  | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | 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= | ||||||
							
								
								
									
										52
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  |  | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  |  | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/config" | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/daemon" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	version string | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func main() { | ||||||
|  |  | ||||||
|  | 	switch os.Getenv("DYNDNS_CLIENT_LOGGER_LEVEL") { | ||||||
|  | 	case "DEBUG", "debug": | ||||||
|  | 		log.SetLevel(log.DebugLevel) | ||||||
|  | 	case "WARN", "warn": | ||||||
|  | 		log.SetLevel(log.WarnLevel) | ||||||
|  | 	case "ERROR", "error": | ||||||
|  | 		log.SetLevel(log.ErrorLevel) | ||||||
|  | 	case "FATAL", "fatal": | ||||||
|  | 		log.SetLevel(log.FatalLevel) | ||||||
|  | 	case "INFO", "info": | ||||||
|  | 		fallthrough | ||||||
|  | 	default: | ||||||
|  | 		log.SetLevel(log.InfoLevel) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch os.Getenv("DYNDNS_CLIENT_LOGGER_FORMATTER") { | ||||||
|  | 	case "JSON", "json": | ||||||
|  | 		log.SetFormatter(&log.JSONFormatter{}) | ||||||
|  | 	case "TEXT", "text": | ||||||
|  | 		fallthrough | ||||||
|  | 	default: | ||||||
|  | 		log.SetFormatter(&log.TextFormatter{ | ||||||
|  | 			FullTimestamp: true, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Infof("version %v", version) | ||||||
|  |  | ||||||
|  | 	cnf, err := config.Read("/etc/dyndns-client/config.json") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal(err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	daemon.Start(cnf) | ||||||
|  | } | ||||||
							
								
								
									
										147
									
								
								pkg/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								pkg/config/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | package config | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	_ "embed" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/types" | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | //go:embed config.json | ||||||
|  | var defaultConfig string | ||||||
|  |  | ||||||
|  | // GetDefaultConfiguration returns a default configuration | ||||||
|  | func GetDefaultConfiguration() (*types.Config, error) { | ||||||
|  | 	cnf := new(types.Config) | ||||||
|  | 	jsonDecoder := json.NewDecoder(strings.NewReader(defaultConfig)) | ||||||
|  |  | ||||||
|  | 	err := jsonDecoder.Decode(cnf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to decode default config: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defaultInterface, err := getDefaultInterfaceByIP() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	cnf.Ifaces = []string{defaultInterface.Name} | ||||||
|  |  | ||||||
|  | 	return cnf, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Read config from a file | ||||||
|  | func Read(cnfFile string) (*types.Config, error) { | ||||||
|  |  | ||||||
|  | 	// Load burned in configuration if config not available | ||||||
|  | 	if _, err := os.Stat(cnfFile); os.IsNotExist(err) { | ||||||
|  | 		if err := os.MkdirAll(filepath.Dir(cnfFile), 0755); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("failed to create directory: %w", err) | ||||||
|  | 		} | ||||||
|  | 		cnf, err := GetDefaultConfiguration() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err = cnf.Validate() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		log.Infof("use embedded configuration") | ||||||
|  |  | ||||||
|  | 		return cnf, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f, err := os.Open(cnfFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to open file: %w", err) | ||||||
|  | 	} | ||||||
|  | 	defer f.Close() | ||||||
|  |  | ||||||
|  | 	cnf := new(types.Config) | ||||||
|  | 	jsonDecoder := json.NewDecoder(f) | ||||||
|  | 	err = jsonDecoder.Decode(cnf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to decode json: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, iface := range cnf.Ifaces { | ||||||
|  | 		if _, err := net.InterfaceByName(iface); err != nil { | ||||||
|  | 			return nil, fmt.Errorf("unknown interface: %v", iface) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = cnf.Validate() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	log.Infof("use configuration from file %v", cnfFile) | ||||||
|  |  | ||||||
|  | 	return cnf, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Write config into a file | ||||||
|  | func Write(cnf *types.Config, cnfFile string) error { | ||||||
|  | 	if _, err := os.Stat(filepath.Dir(cnfFile)); os.IsNotExist(err) { | ||||||
|  | 		err := os.MkdirAll(filepath.Dir(cnfFile), 0755) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	f, err := os.Create(cnfFile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to create file %v: %v", cnfFile, err) | ||||||
|  | 	} | ||||||
|  | 	defer f.Close() | ||||||
|  |  | ||||||
|  | 	jsonEncoder := json.NewEncoder(f) | ||||||
|  | 	jsonEncoder.SetIndent("", "  ") | ||||||
|  | 	err = jsonEncoder.Encode(cnf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to encode json: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getDefaultInterfaceByIP() (*net.Interface, error) { | ||||||
|  | 	ifaces, err := net.Interfaces() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to fet network interfaces from kernel: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	defaultIP := getOutboundIP() | ||||||
|  |  | ||||||
|  | 	for _, iface := range ifaces { | ||||||
|  | 		addrs, err := iface.Addrs() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("failed to list ip addresses for interface %v: %w", iface.Name, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, addr := range addrs { | ||||||
|  | 			addrIP := strings.Split(addr.String(), "/")[0] | ||||||
|  | 			if addrIP == defaultIP.String() { | ||||||
|  | 				return &iface, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil, fmt.Errorf("no interface found fo ip address %v", defaultIP) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getOutboundIP() net.IP { | ||||||
|  | 	conn, err := net.Dial("udp", "8.8.8.8:80") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	defer conn.Close() | ||||||
|  | 	localAddr := conn.LocalAddr().(*net.UDPAddr) | ||||||
|  | 	return localAddr.IP | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								pkg/config/config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pkg/config/config.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | {} | ||||||
							
								
								
									
										335
									
								
								pkg/daemon/daemon.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								pkg/daemon/daemon.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,335 @@ | |||||||
|  | package daemon | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"os/signal" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"syscall" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/types" | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/updater" | ||||||
|  | 	"github.com/asaskevich/govalidator" | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"github.com/vishvananda/netlink" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Start(cnf *types.Config) { | ||||||
|  | 	addrUpdates := make(chan netlink.AddrUpdate, 1) | ||||||
|  | 	done := make(chan struct{}, 1) | ||||||
|  | 	err := netlink.AddrSubscribeWithOptions(addrUpdates, done, netlink.AddrSubscribeOptions{ | ||||||
|  | 		ListExisting: true, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("failed to subscribe netlink notifications from kernel: %v", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	interuptChannel := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(interuptChannel, syscall.SIGINT, syscall.SIGTERM) | ||||||
|  |  | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	daemonCtx, cancle := context.WithCancel(ctx) | ||||||
|  | 	defer cancle() | ||||||
|  |  | ||||||
|  | 	updaters, err := getUpdaterForEachZone(cnf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Fatalf("%v", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := pruneRecords(daemonCtx, updaters, cnf.Zones); err != nil { | ||||||
|  | 		log.Fatalf("%v", err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		interfaces, err := netlink.LinkList() | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Fatal("%v", err.Error()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		select { | ||||||
|  | 		case update := <-addrUpdates: | ||||||
|  |  | ||||||
|  | 			interfaceLogger := log.WithFields(log.Fields{ | ||||||
|  | 				"ip": update.LinkAddress.IP.String(), | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			// search interface by index | ||||||
|  | 			iface, err := searchInterfaceByIndex(update.LinkIndex, interfaces) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Errorf("%v", err.Error()) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			interfaceLogger = interfaceLogger.WithField("device", iface.Attrs().Name) | ||||||
|  |  | ||||||
|  | 			var recordType string | ||||||
|  | 			switch { | ||||||
|  | 			case govalidator.IsIPv4(strings.TrimRight(update.LinkAddress.IP.String(), "/")): | ||||||
|  | 				recordType = "A" | ||||||
|  | 			case govalidator.IsIPv6(strings.TrimRight(update.LinkAddress.IP.String(), "/")): | ||||||
|  | 				recordType = "AAAA" | ||||||
|  | 			default: | ||||||
|  | 				interfaceLogger.Error("failed to detect record type") | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			interfaceLogger = interfaceLogger.WithField("rr", recordType) | ||||||
|  |  | ||||||
|  | 			interfaceLogger.Debug("receive kernel notification for interface") | ||||||
|  |  | ||||||
|  | 			// filter out not configured interfaces | ||||||
|  | 			if !matchInterfaces(iface.Attrs().Name, cnf.Ifaces) { | ||||||
|  | 				interfaceLogger.Warn("interface is not part of the allowed interface list") | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// filter out notification for a bad interface ip address, for example link-local-addresses | ||||||
|  | 			if update.LinkAddress.IP.IsLoopback() || strings.HasPrefix(update.LinkAddress.IP.String(), "fe80") { | ||||||
|  | 				interfaceLogger.Warn("interface is a loopback device or part of a loopback network") | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// decide if trigger a add or delete event | ||||||
|  | 			if update.NewAddr { | ||||||
|  | 				err = addIPRecords(daemonCtx, interfaceLogger, updaters, cnf.Zones, recordType, update.LinkAddress.IP) | ||||||
|  | 				if err != nil { | ||||||
|  | 					interfaceLogger.Error(err.Error()) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				err = removeIPRecords(daemonCtx, interfaceLogger, updaters, cnf.Zones, recordType, update.LinkAddress.IP) | ||||||
|  | 				if err != nil { | ||||||
|  | 					interfaceLogger.Error(err.Error()) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		case killSignal := <-interuptChannel: | ||||||
|  | 			log.Debugf("got signal: %v", killSignal) | ||||||
|  | 			log.Debugf("daemon was killed by: %v", killSignal) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getUpdaterForEachZone(config *types.Config) (map[string]updater.Updater, error) { | ||||||
|  | 	updaterCollection := make(map[string]updater.Updater) | ||||||
|  |  | ||||||
|  | 	for zoneName, zone := range config.Zones { | ||||||
|  | 		nsUpdater, err := updater.NewNSUpdate(zone.DNSServer, config.TSIGKeys[zone.TSIGKeyName]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		updaterCollection[zoneName] = nsUpdater | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return updaterCollection, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func matchInterfaces(iface string, ifaces []string) bool { | ||||||
|  | 	for _, i := range ifaces { | ||||||
|  | 		if i == iface { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func searchInterfaceByIndex(index int, interfaces []netlink.Link) (netlink.Link, error) { | ||||||
|  | 	for _, iface := range interfaces { | ||||||
|  | 		if iface.Attrs().Index == index { | ||||||
|  | 			return iface, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil, fmt.Errorf("can not find interface by index %v", index) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func addIPRecords(ctx context.Context, logEntry *log.Entry, updaters map[string]updater.Updater, zones map[string]*types.Zone, recordType string, ip net.IP) error { | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		errorChannel = make(chan error, len(zones)) | ||||||
|  | 		wg           = new(sync.WaitGroup) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	hostname, err := os.Hostname() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to get host name from kernel: %w", err) | ||||||
|  | 	} | ||||||
|  | 	hostname = strings.ToLower(hostname) | ||||||
|  |  | ||||||
|  | 	if !verifyHostname(hostname) { | ||||||
|  | 		return fmt.Errorf("host name not valid: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for zoneName := range zones { | ||||||
|  | 		wg.Add(1) | ||||||
|  |  | ||||||
|  | 		go func(ctx context.Context, zoneName string, hostname string, recordType string, ip net.IP, wg *sync.WaitGroup) { | ||||||
|  |  | ||||||
|  | 			zoneLogger := logEntry.WithFields(log.Fields{ | ||||||
|  | 				"zone":     zoneName, | ||||||
|  | 				"hostname": hostname, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			defer wg.Done() | ||||||
|  |  | ||||||
|  | 			pruneRecordCtx, cancle := context.WithTimeout(ctx, time.Second*15) | ||||||
|  | 			defer cancle() | ||||||
|  |  | ||||||
|  | 			fqdn := fmt.Sprintf("%v.%v", hostname, zoneName) | ||||||
|  |  | ||||||
|  | 			err := updaters[zoneName].AddRecord(pruneRecordCtx, fqdn, 60, recordType, ip.String()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				errorChannel <- fmt.Errorf("failed to remove record type %v for %v: %v", recordType, fqdn, err.Error()) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			zoneLogger.Info("dns-record successfully updated") | ||||||
|  |  | ||||||
|  | 		}(ctx, zoneName, hostname, recordType, ip, wg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wg.Wait() | ||||||
|  | 	close(errorChannel) | ||||||
|  |  | ||||||
|  | 	for err := range errorChannel { | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func pruneRecords(ctx context.Context, updaters map[string]updater.Updater, zones map[string]*types.Zone) error { | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		errorChannel = make(chan error, len(zones)) | ||||||
|  | 		wg           = new(sync.WaitGroup) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	hostname, err := os.Hostname() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to get host name from kernel: %w", err) | ||||||
|  | 	} | ||||||
|  | 	hostname = strings.ToLower(hostname) | ||||||
|  |  | ||||||
|  | 	if !verifyHostname(hostname) { | ||||||
|  | 		return fmt.Errorf("host name not valid: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for zoneName := range zones { | ||||||
|  | 		wg.Add(1) | ||||||
|  |  | ||||||
|  | 		go func(zoneName string, hostname string, errorChannel chan<- error, wg *sync.WaitGroup) { | ||||||
|  | 			defer wg.Done() | ||||||
|  |  | ||||||
|  | 			pruneRecordCtx, cancle := context.WithTimeout(ctx, time.Second*15) | ||||||
|  | 			defer cancle() | ||||||
|  |  | ||||||
|  | 			fqdn := fmt.Sprintf("%v.%v", hostname, zoneName) | ||||||
|  |  | ||||||
|  | 			err := updaters[zoneName].PruneRecords(pruneRecordCtx, fqdn) | ||||||
|  | 			if err != nil { | ||||||
|  | 				errorChannel <- fmt.Errorf("failed to prune %v: %v", fqdn, err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		}(zoneName, hostname, errorChannel, wg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wg.Wait() | ||||||
|  | 	close(errorChannel) | ||||||
|  |  | ||||||
|  | 	for err := range errorChannel { | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func removeIPRecords(ctx context.Context, logEntry *log.Entry, updaters map[string]updater.Updater, zones map[string]*types.Zone, recordType string, ip net.IP) error { | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		errorChannel = make(chan error, len(zones)) | ||||||
|  | 		wg           = new(sync.WaitGroup) | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	hostname, err := os.Hostname() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("failed to get host name from kernel: %w", err) | ||||||
|  | 	} | ||||||
|  | 	hostname = strings.ToLower(hostname) | ||||||
|  |  | ||||||
|  | 	if !verifyHostname(hostname) { | ||||||
|  | 		return fmt.Errorf("host name not valid: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for zoneName := range zones { | ||||||
|  | 		wg.Add(1) | ||||||
|  |  | ||||||
|  | 		go func(ctx context.Context, zoneName string, hostname string, recordType string, wg *sync.WaitGroup) { | ||||||
|  | 			defer wg.Done() | ||||||
|  |  | ||||||
|  | 			zoneLogger := logEntry.WithFields(log.Fields{ | ||||||
|  | 				"zone":     zoneName, | ||||||
|  | 				"hostname": hostname, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			pruneRecordCtx, cancle := context.WithTimeout(ctx, time.Second*15) | ||||||
|  | 			defer cancle() | ||||||
|  |  | ||||||
|  | 			fqdn := fmt.Sprintf("%v.%v", hostname, zoneName) | ||||||
|  |  | ||||||
|  | 			err := updaters[zoneName].DeleteRecord(pruneRecordCtx, fqdn, recordType) | ||||||
|  | 			if err != nil { | ||||||
|  | 				errorChannel <- fmt.Errorf("failed to remove record type %v for %v: %v", recordType, fqdn, err.Error()) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			zoneLogger.Info("dns-record successfully removed") | ||||||
|  |  | ||||||
|  | 		}(ctx, zoneName, hostname, recordType, wg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	wg.Wait() | ||||||
|  | 	close(errorChannel) | ||||||
|  |  | ||||||
|  | 	for err := range errorChannel { | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // verifyHostname returns a boolean if the hostname id valid. The hostname does | ||||||
|  | // not contains any dot or local, localhost, localdomain. | ||||||
|  | func verifyHostname(hostname string) bool { | ||||||
|  |  | ||||||
|  | 	if !validHostname.MatchString(hostname) { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	hostnames := []string{ | ||||||
|  | 		"local", | ||||||
|  | 		"localhost", | ||||||
|  | 		"localdomain", | ||||||
|  | 		"orbisos", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := range hostnames { | ||||||
|  | 		if hostnames[i] == hostname { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	validHostname = regexp.MustCompile(`^[a-zA-Z0-9]+([\-][a-zA-Z0-9]+)*$`) | ||||||
|  | ) | ||||||
							
								
								
									
										42
									
								
								pkg/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								pkg/types/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"github.com/asaskevich/govalidator" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Config struct { | ||||||
|  | 	Ifaces   []string            `json:"interfaces"` | ||||||
|  | 	Zones    map[string]*Zone    `json:"zones"` | ||||||
|  | 	TSIGKeys map[string]*TSIGKey `json:"tsig-keys"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c *Config) Validate() error { | ||||||
|  | 	if len(c.Zones) <= 0 { | ||||||
|  | 		return fmt.Errorf("no dns zones configured") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(c.Ifaces) <= 0 { | ||||||
|  | 		return fmt.Errorf("no interfaces configured") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | ZONE_LOOP: | ||||||
|  | 	for _, zone := range c.Zones { | ||||||
|  | 		if !govalidator.IsIP(zone.DNSServer) && !govalidator.IsDNSName(zone.DNSServer) { | ||||||
|  | 			return fmt.Errorf("invalid dns server %v", zone.DNSServer) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !govalidator.IsDNSName(zone.Name) { | ||||||
|  | 			return fmt.Errorf("invalid dns zone name %v", zone.Name) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, tsigkey := range c.TSIGKeys { | ||||||
|  | 			if tsigkey.Name == zone.TSIGKeyName { | ||||||
|  | 				continue ZONE_LOOP | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return fmt.Errorf("no matching tsigkey found for zone %v", zone.Name) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								pkg/types/tsigkey.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pkg/types/tsigkey.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | type TSIGKey struct { | ||||||
|  | 	Algorithm string `json:"algorithm"` | ||||||
|  | 	Name      string `json:"name"` | ||||||
|  | 	Secret    string `json:"secret"` | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								pkg/types/zone.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pkg/types/zone.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | type Zone struct { | ||||||
|  | 	DNSServer   string `json:"dns-server"` | ||||||
|  | 	Name        string `json:"name"` | ||||||
|  | 	TSIGKeyName string `json:"tsig-key"` | ||||||
|  | } | ||||||
							
								
								
									
										72
									
								
								pkg/updater/updater.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								pkg/updater/updater.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | package updater | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.cryptic.systems/volker.raschek/dyndns-client/pkg/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Updater interface { | ||||||
|  | 	AddRecord(ctx context.Context, name string, ttl uint, recordType string, data string) error | ||||||
|  | 	DeleteRecord(ctx context.Context, name string, recordType string) error | ||||||
|  | 	PruneRecords(ctx context.Context, name string) error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type NSUpdate struct { | ||||||
|  | 	server  string | ||||||
|  | 	tsigKey *types.TSIGKey | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *NSUpdate) AddRecord(ctx context.Context, name string, ttl uint, recordType string, data string) error { | ||||||
|  | 	nsUpdateCmd := "" | ||||||
|  | 	switch recordType { | ||||||
|  | 	case "A", "AAAA": | ||||||
|  | 		nsUpdateCmd = fmt.Sprintf("update add %v %v IN %v %v", name, ttl, recordType, data) | ||||||
|  | 	case "TXT": | ||||||
|  | 		nsUpdateCmd = fmt.Sprintf("update add %v %v IN %v \"%v\"", name, ttl, recordType, data) | ||||||
|  | 	default: | ||||||
|  | 		return fmt.Errorf("RecordType %v not supported", recordType) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return u.execute(ctx, nsUpdateCmd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *NSUpdate) DeleteRecord(ctx context.Context, name string, recordType string) error { | ||||||
|  | 	nsUpdateCmd := fmt.Sprintf("update delete %v IN %v", name, recordType) | ||||||
|  | 	return u.execute(ctx, nsUpdateCmd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *NSUpdate) execute(ctx context.Context, nsUpdateCmd string) error { | ||||||
|  | 	body := fmt.Sprintf("server %v\n%v\nsend\nquit", u.server, nsUpdateCmd) | ||||||
|  |  | ||||||
|  | 	errBuffer := new(bytes.Buffer) | ||||||
|  |  | ||||||
|  | 	cmd := exec.CommandContext(ctx, "nsupdate", "-y", fmt.Sprintf("%v:%v:%v", u.tsigKey.Algorithm, u.tsigKey.Name, u.tsigKey.Secret)) | ||||||
|  | 	// cmd.Stdout = os.Stdout | ||||||
|  | 	cmd.Stderr = bufio.NewWriter(errBuffer) | ||||||
|  | 	cmd.Stdin = strings.NewReader(body) | ||||||
|  |  | ||||||
|  | 	err := cmd.Run() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("nsupdate error %w: %v", err, errBuffer.String()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *NSUpdate) PruneRecords(ctx context.Context, name string) error { | ||||||
|  | 	nsUpdateCmd := fmt.Sprintf("update delete %v", name) | ||||||
|  | 	return u.execute(ctx, nsUpdateCmd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewNSUpdate(server string, tsigKey *types.TSIGKey) (Updater, error) { | ||||||
|  | 	return &NSUpdate{ | ||||||
|  | 		server:  server, | ||||||
|  | 		tsigKey: tsigKey, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								systemd/dyndns-client-docker.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								systemd/dyndns-client-docker.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | [Unit] | ||||||
|  | Description=dyndns-client | ||||||
|  | Requires=docker.service network-online.target time-sync.target | ||||||
|  | After=docker.service network-online.target time-sync.target | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | Type=simple | ||||||
|  | ExecStartPre=/usr/bin/docker pull docker.io/volkerraschek/dyndns-client:latest | ||||||
|  | ExecStart=/usr/bin/docker run --name=dyndns-client --network=host --privileged --rm --volume /etc/dyndns-client:/etc/dyndns-client docker.io/volkerraschek/dyndns-client:latest | ||||||
|  | ExecStop=/usr/bin/docker stop dyndns-client | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
							
								
								
									
										13
									
								
								systemd/dyndns-client.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								systemd/dyndns-client.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | [Unit] | ||||||
|  | Description=dyndns-client | ||||||
|  | Requires=network-online.target time-sync.target | ||||||
|  | After=network-online.target time-sync.target | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | Type=simple | ||||||
|  | Environment=DYNDNS_CLIENT_LOGGER_LEVEL=INFO | ||||||
|  | ExecStart=/usr/bin/dyndns-client | ||||||
|  | KillSignal=SIGTERM | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
		Reference in New Issue
	
	Block a user