initial commit

This commit is contained in:
W Anders
2024-05-06 17:37:57 -06:00
commit 12bcac933a
35 changed files with 3027 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
name: Build and Test
on:
push:
branches:
- master
- fix-*
- feat-*
- update-*
paths:
- '**.go'
- go.mod
- go.sum
pull_request:
types:
- opened
- synchronize
- reopened
paths:
- '**.go'
- go.mod
- go.sum
jobs:
build-test-publish-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- run: go build
- run: |
docker compose -p coredns-netbox-plugin-dns -f ${{ github.workspace }}/.testing/docker-compose.yml up -d && \
until [[ "`docker inspect -f {{.State.Health.Status}} coredns-netbox-plugin-dns-netbox-1`" == "healthy" ]]; do
echo "Waiting for Netbox to come online..."
sleep 5
done && \
go run ${{ github.workspace }}/.testing/init/init.go
- run: go test -coverprofile='coverage.out'
- uses: sonarsource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.organization=doubleu-labs
-Dsonar.projectKey=doubleu-labs_coredns-netbox-plugin-dns
-Dsonar.go.coverage.reportPaths=coverage.out
-Dsonar.verbose=true
-Dsonar.sources=.
-Dsonar.exclusions=**/*_test.go,.testing/*
-Dsonar.tests=.
-Dsonar.test.inclusions=**/*_test.go
+1
View File
@@ -0,0 +1 @@
coverage.out
+11
View File
@@ -0,0 +1,11 @@
PLUGINS = [
'netbox_dns',
]
PLUGINS_CONFIG = {
'netbox_dns': {
'feature_ipam_coupling': True,
'tolerate_underscores_in_hostnames': True,
},
}
+39
View File
@@ -0,0 +1,39 @@
services:
netbox:
image: localhost/netbox-plugin-dns:latest
build:
context: .
dockerfile: ./netbox.Containerfile
depends_on:
- postgres
- redis
- redis-cache
env_file: env/netbox.env
user: 'unit:root'
healthcheck:
start_period: 60s
timeout: 3s
interval: 15s
test: "curl -f http://localhost:8080/api/ || exit 1"
ports:
- "9999:8080"
postgres:
image: docker.io/library/postgres:16
env_file: env/postgres.env
redis:
image: docker.io/library/redis:7
command:
- sh
- -c
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD
env_file: env/redis.env
redis-cache:
image: docker.io/library/redis:7
command:
- sh
- -c
- redis-server --requirepass $$REDIS_PASSWORD
env_file: env/redis-cache.env
+34
View File
@@ -0,0 +1,34 @@
CORS_ORIGIN_ALLOW_ALL=True
DB_HOST=postgres
DB_NAME=netbox
DB_PASSWORD=J5brHrAXFLQSif0K
DB_USER=netbox
EMAIL_FROM=netbox@bar.com
EMAIL_PASSWORD=
EMAIL_PORT=25
EMAIL_SERVER=localhost
EMAIL_SSL_CERTFILE=
EMAIL_SSL_KEYFILE=
EMAIL_TIMEOUT=5
EMAIL_USERNAME=netbox
# EMAIL_USE_SSL and EMAIL_USE_TLS are mutually exclusive, i.e. they can't both be `true`!
EMAIL_USE_SSL=false
EMAIL_USE_TLS=false
GRAPHQL_ENABLED=true
HOUSEKEEPING_INTERVAL=86400
MEDIA_ROOT=/opt/netbox/netbox/media
METRICS_ENABLED=false
REDIS_CACHE_DATABASE=1
REDIS_CACHE_HOST=redis-cache
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
REDIS_CACHE_PASSWORD=t4Ph722qJ5QHeQ1qfu36
REDIS_CACHE_SSL=false
REDIS_DATABASE=0
REDIS_HOST=redis
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_PASSWORD=H733Kdjndks81
REDIS_SSL=false
RELEASE_CHECK_URL=https://api.github.com/repos/netbox-community/netbox/releases
SECRET_KEY='r(m)9nLGnz$(_q3N4z1k(EFsMCjjjzx08x9VhNVcfd%6RF#r!6DE@+V5Zk2X'
WEBHOOKS_ENABLED=true
SUPERUSER_API_TOKEN=w5pgWXPqZVmngLN4w4XwuPvZfUC72ytDxnnHgEmI
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_DB=netbox
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
POSTGRES_USER=netbox
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
+1
View File
@@ -0,0 +1 @@
REDIS_PASSWORD=H733Kdjndks81
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
)
var (
apiRoot = "http://localhost:9999/api/plugins/netbox-dns"
token = "w5pgWXPqZVmngLN4w4XwuPvZfUC72ytDxnnHgEmI"
execdir string
)
func init() {
_, filename, _, ok := runtime.Caller(0)
if !ok {
panic("unable to get current filename")
}
execdir = filepath.Dir(filename)
}
func post(client *http.Client, path string, filepath string) (string, []byte) {
file, err := os.Open(filepath)
if err != nil {
log.Fatal(err)
}
defer file.Close()
stat, _ := file.Stat()
req, err := http.NewRequest("POST", apiRoot+path, file)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", fmt.Sprintf("Token %s", token))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json; indent=4")
req.ContentLength = stat.Size()
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
content, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
return resp.Status, content
}
func main() {
nameservers := filepath.Join(execdir, "nameservers.json")
zones := filepath.Join(execdir, "zones.json")
records := filepath.Join(execdir, "records.json")
client := &http.Client{}
nsStatus, nsContent := post(client, "/nameservers/", nameservers)
log.Printf("nameservers: %s\n%s", nsStatus, nsContent)
zoneStatus, zoneContent := post(client, "/zones/", zones)
log.Printf("zones: %s\n%s", zoneStatus, zoneContent)
recordStatus, recordContent := post(client, "/records/", records)
log.Printf("records: %s\n%s", recordStatus, recordContent)
}
+8
View File
@@ -0,0 +1,8 @@
[
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
]
+250
View File
@@ -0,0 +1,250 @@
[
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "dns01",
"value": "10.0.0.10"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "dns01",
"value": "2001:db8:dead:beef::1:10"
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "dns02",
"value": "10.0.0.11"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "dns02",
"value": "2001:db8:dead:beef::1:11"
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "aservice",
"value": "10.0.0.12"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "aservice",
"value": "2001:db8:dead:beef::1:12"
},
{
"zone": {
"name": "example.com"
},
"type": "MX",
"name": "@",
"value": "10 mail.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "mail",
"value": "10.0.0.13"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "mail",
"value": "2001:db8:dead:beef::1:13"
},
{
"zone": {
"name": "example.com"
},
"type": "TXT",
"name": "@",
"value": "v=spf1 ip4:10.0.0.13 ip6:2001:db8:dead:beef::1:13 a -all"
},
{
"zone": {
"name": "example.com"
},
"type": "TXT",
"name": "@",
"value": "v=DMARC1;p=none;sp=quarantine;pct=100;rua=admin@example.com;"
},
{
"zone": {
"name": "example.com"
},
"type": "TXT",
"name": "@",
"value": "\"some value\"\\r\\n\"another value\""
},
{
"zone": {
"name": "example.com"
},
"type": "TXT",
"name": "@",
"value": "\"newline record\"\\n\"second value\""
},
{
"zone": {
"name": "example.com"
},
"type": "TXT",
"name": "@",
"value": "\"my value\" \"second my value\" \"third my value\""
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "puppet-server-a",
"value": "10.0.0.15"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "puppet-server-a",
"value": "2001:db8:dead:beef::1:15"
},
{
"zone": {
"name": "example.com"
},
"type": "SRV",
"name": "_x-puppet._tcp",
"value": "0 5 8140 puppet-server-a.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "puppet-server-b",
"value": "10.0.0.16"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "puppet-server-b",
"value": "2001:db8:dead:beef::1:16"
},
{
"zone": {
"name": "example.com"
},
"type": "SRV",
"name": "_x-puppet._tcp",
"value": "0 5 8140 puppet-server-b.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "A",
"name": "web",
"value": "10.0.0.17"
},
{
"zone": {
"name": "example.com"
},
"type": "AAAA",
"name": "web",
"value": "2001:db8:dead:beef::1:17"
},
{
"zone": {
"name": "example.com"
},
"type": "CNAME",
"name": "www",
"value": "web.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "NS",
"name": "sub",
"value": "dns01.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "NS",
"name": "sub",
"value": "dns02.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "NS",
"name": "subtwo",
"value": "dns01.example.com"
},
{
"zone": {
"name": "example.com"
},
"type": "NS",
"name": "subtwo",
"value": "dns02.example.com"
},
{
"zone": {
"name": "sub.example.com"
},
"type": "A",
"name": "myservice",
"value": "10.0.1.10"
},
{
"zone": {
"name": "sub.example.com"
},
"type": "AAAA",
"name": "myservice",
"value": "2001:db8:dead:beef::2:10"
},
{
"zone": {
"name": "subtwo.example.com"
},
"type": "A",
"name": "myotherservice",
"value": "10.0.2.10"
},
{
"zone": {
"name": "subtwo.example.com"
},
"type": "AAAA",
"name": "myotherservice",
"value": "2001:db8:dead:beef::3:10"
}
]
+209
View File
@@ -0,0 +1,209 @@
[
{
"name": "example.com",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "0.0.10.in-addr.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "sub.example.com",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "1.0.10.in-addr.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "subtwo.example.com",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "2.0.10.in-addr.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
},
{
"name": "3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa",
"nameservers": [
{
"name": "dns01.example.com"
},
{
"name": "dns02.example.com"
}
],
"default_ttl": 3600,
"soa_expire": 2419200,
"soa_minimum": 3600,
"soa_mname": {
"name": "dns01.example.com"
},
"soa_ttl": 86400,
"soa_refresh": 43200,
"soa_retry": 7200,
"soa_rname": "admin.example.com",
"soa_serial_auto": false,
"soa_serial": 1
}
]
+13
View File
@@ -0,0 +1,13 @@
FROM docker.io/netboxcommunity/netbox:latest
COPY ./requirements-plugin.txt /opt/netbox/
RUN /opt/netbox/venv/bin/pip install \
--no-warn-script-location \
-r /opt/netbox/requirements-plugin.txt
COPY configuration/plugins.py /etc/netbox/config/plugins.py
RUN SECRET_KEY="dummydummydummydummydummydummydummydummydummydummy" \
/opt/netbox/venv/bin/python \
/opt/netbox/netbox/manage.py \
collectstatic --no-input
+1
View File
@@ -0,0 +1 @@
netbox-plugin-dns >= 0.22.8
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
docker compose -p coredns-netbox-plugin-dns -f ./.testing/docker-compose.yml up -d && \
until [[ "`docker inspect -f {{.State.Health.Status}} coredns-netbox-plugin-dns-netbox-1`" == "healthy" ]]; do
echo "Waiting for Netbox to come online..."
sleep 5;
done && \
go run ./.testing/init/init.go
+14
View File
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICHDCCAcKgAwIBAgIUKqkR+rk8a0RIfpAdkRRr/jgdhhQwCgYIKoZIzj0EAwIw
bDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
c2NvMRcwFQYDVQQKEw5OZXRib3ggVGVzdGluZzEfMB0GA1UEAxMWTmV0Ym94IFRl
c3RpbmcgUm9vdCBDQTAeFw0yNDA1MDIwMDU1MDBaFw0yOTA1MDEwMDU1MDBaMGwx
CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
bzEXMBUGA1UEChMOTmV0Ym94IFRlc3RpbmcxHzAdBgNVBAMTFk5ldGJveCBUZXN0
aW5nIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARInLLhQRy22ueR
FMUVzpzhllZkEkuOMQzDR83X9B5oKWWIhbEtjILLYOigf4BstV4O4NoaNbTF9gON
PRtkVL0ao0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQU7MXIkjUgpd/cvpDUOxIDczDKFR4wCgYIKoZIzj0EAwIDSAAwRQIhAPWc
++vwGVv0BNU2bWNLDQO8/T0izNv78rHfNuxKx/+OAiATSCSs7yDId9RW02sADVrL
4JeoHAlc/qsMltuTeXV+kA==
-----END CERTIFICATE-----
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEILxY99DUkfyC9uFgkLzJoce2BkEwxI2FiBttKptbOFgBoAoGCCqGSM49
AwEHoUQDQgAEM1w4sKz9to1SpdZ5whJK41t5JVAYivmFklD87IAQOKXqt5DKAX9r
Z8f/95FVt8qGOYkG4OYP4sCfi8g2pnd6Jg==
-----END EC PRIVATE KEY-----
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICTzCCAfSgAwIBAgIUaz+i0MMtm6NZ73aB6OPZ7V4N8WIwCgYIKoZIzj0EAwIw
bDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
c2NvMRcwFQYDVQQKEw5OZXRib3ggVGVzdGluZzEfMB0GA1UEAxMWTmV0Ym94IFRl
c3RpbmcgUm9vdCBDQTAeFw0yNDA1MDIwMTA0MDBaFw0yOTA1MDEwMTA0MDBaMGsx
CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNj
bzEXMBUGA1UEChMOTmV0Ym94IFRlc3RpbmcxHjAcBgNVBAMTFU5ldGJveCBUZXN0
aW5nIENsaWVudDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDNcOLCs/baNUqXW
ecISSuNbeSVQGIr5hZJQ/OyAEDil6reQygF/a2fH//eRVbfKhjmJBuDmD+LAn4vI
NqZ3eiajdTBzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjAM
BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTo33pMbsD0Qe4bfVoEA850oKgqEzAfBgNV
HSMEGDAWgBTsxciSNSCl39y+kNQ7EgNzMMoVHjAKBggqhkjOPQQDAgNJADBGAiEA
qLETeHL3iuG1Vxdey+VhEU4q5Xfp59mvR6YJksBT3oECIQCRSDRSo0t9nQh6U9wg
C/KvjPFLc0pYblQiiuQOlDtjXg==
-----END CERTIFICATE-----
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Test",
"type": "go",
"request": "launch",
"mode": "test",
"program": "${workspaceFolder}"
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"go.testFlags": ["-v"]
}
+80
View File
@@ -0,0 +1,80 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "[Start] Netbox test instance",
"type": "docker-compose",
"dockerCompose": {
"projectName": "coredns-netbox-plugin-dns",
"up": {
"detached": true,
"build": true,
},
"files": [
"${workspaceFolder}/.testing/docker-compose.yml"
]
}
},
{
"label": "[Stop] Netbox test instance",
"type": "docker-compose",
"dockerCompose": {
"projectName": "coredns-netbox-plugin-dns",
"down": {
"removeVolumes": true
},
"files": [
"${workspaceFolder}/.testing/docker-compose.yml"
]
}
},
{
"label": "Go Coverage",
"type": "shell",
"command": "go",
"args": [
"test",
"-short",
"-coverprofile=${workspaceFolder}/coverage.out",
"./..."
],
"options": {
"env": {
"CGO_ENABLED": "0"
}
},
"problemMatcher": {
"pattern": {
"regexp": ".*"
}
}
},
{
"label": "Go View Coverage",
"type": "shell",
"linux": {
"command": "setsid",
"args": [
"go",
"tool",
"cover",
"-html=${workspaceFolder}/coverage.out"
]
},
"windows": {
"command": "go",
"args": [
"tool",
"cover",
"-html=${workspaceFolder}/coverage.out"
]
},
"dependsOn": "Go Coverage",
"problemMatcher": {
"pattern": {
"regexp": ".*"
}
}
},
]
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 W Anders <w@doubleu.codes>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+149
View File
@@ -0,0 +1,149 @@
# netboxdns
[![Go Reference](https://pkg.go.dev/badge/github.com/doubleu-labs/coredns-netbox-plugin-dns.svg)](https://pkg.go.dev/github.com/doubleu-labs/coredns-netbox-plugin-dns)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=doubleu-labs_coredns-netbox-plugin-dns&metric=coverage)](https://sonarcloud.io/summary/overall?id=doubleu-labs_coredns-netbox-plugin-dns)
[![Go Report Card](https://goreportcard.com/badge/github.com/doubleu-labs/coredns-netbox-plugin-dns)](https://goreportcard.com/report/github.com/doubleu-labs/coredns-netbox-plugin-dns)
*netboxdns* - provides resolution using
[Netbox DNS Plugin (netbox-plugin-dns)](https://github.com/peteeckel/netbox-plugin-dns)
## Description
The *netboxdns* plugin provides resolution for zones configured using
[netbox-plugin-dns](https://github.com/peteeckel/netbox-plugin-dns).
**Depends on `netbox-plugin-dns` version `0.22.8` or greater.**
The account that the API token is tied to will need the following permissions:
- `netbox_dns.view_zone`
- `netbox_dns.view_record`
## Syntax
Available configuration options:
```nginx
netboxdns [ZONES...] {
token TOKEN
url URL
timeout DURATION
fallthrough [ZONES...]
tls CERT KET CACERT
}
```
* **ZONES**: A space-delimited list of zones that the plugin will answer for
* **`token TOKEN` (REQUIRED)**: The API token used to authenticate requests
to the Netbox instance
* **`url URL` (REQUIRED)**: The URL that Netbox is accessible at
* **`timeout DURATION`** (DEFAULT=`5s`): A duration to time-out requests to the
Netbox API
* **`fallthrough`**: If no record exists, send the request to the next plugin.
* **(OPTIONAL) `ZONES...`**: A space-delimited list of zones that requests
should be forwarded to the next plugin. If requests are not in the specified
zones, an empty reponse is returned.
* **`tls`**: Used to authenticate to the Netbox instance if it is using HTTPS.
* `0 arguments`: Creates a TLS configuration that uses system CA certificates
to validate the connection to the Netbox instance. Use when Netbox is using
a server certificate signed by a public CA. The client is not authenticated
by the server.
* `1 argument`: Path to the CA PEM file. Creates a TLS configuration that uses
the specified CA certificate to validate the connection to the Netbox
instance. Use when Netbox is using a server certificate signed by a private
CA. The client is not authenticated by the server.
* `2 arguments`: Paths to the client certificate and private key PEM files.
Creates a TLS configuration that uses system CA certificates to validate the
connection to the Netbox instance. Use when certificates are needed to
authenticate to the Netbox instance (mTLS) (Netbox Cloud).
* `3 arguments`: Paths to the client certificate, private key, and CA PEM
files. Creates a TLS configuration that uses the specified CA certificate to
validate the connection to the Netbox instance. Use when certificates are
needed to authenticate to the Netbox instance (mTLS) and Netbox is using a
server certificate signed by a private CA.
## Building
Clone the [coredns](https://github.com/coredns/coredns) repository and change
into it's directory.
```sh
git clone https://github.com/coredns/coredns.git
```
```sh
cd coredns
```
Fetch the plugin and add it to `coredns`'s `go.mod` file:
```sh
go get -u github.com/doubleu-labs/coredns-netbox-plugin-dns
```
Update `plugin.cfg` in the root of the directory. The `netboxdns` declaration
should be inserted after `cache` if you want responses from Netbox to be
cached.
```sh
# Using sed
sed -i '/^cache:cache/a netboxdns:github.com/doubleu-labs/coredns-netbox-plugin-dns' plugin.cfg
```
```powershell
# Using Powershell
(Get-Content plugin.cfg).`
Replace("cache:cache", "cache:cache`nnetboxdns:github.com/doubleu-labs/coredns-netbox-plugin-dns") | `
Set-Content -Path plugin.cfg
```
Build using `make`:
```sh
make
```
Or if `make` is not available, simply run:
```sh
go generate && go build
```
The `coredns` binary will be in the root of the project directory, unless
otherwise specified by the `-o` flag.
## Contributing
A [Docker Compose file](./.testing/docker-compose.yml) is provided to setup a
minimal Netbox instance to run tests against. If using Visual Studio Code, two
tasks are configured to start and stop this instance. Use `Ctrl+Shift+P` and
select `[Start] Netbox test instance`.
Check that Netbox is finished with the initial setup by watching the container
logs using:
```sh
docker logs -f coredns-netbox-plugin-dns-netbox-1
```
The test instance will be available at
[http://localhost:9999](http://localhost:9999/) with the `admin:admin` username
and password. When you see healthcheck requests, invoke
[init.go](./.testing/init/init.go) to populate the test dataset.
```sh
go run .testing/init/init.go
```
This standalone application POSTs the contents of the
JSON files in [.testing/init](./.testing/init/) to populate the database. If
adding a new feature or bugfix that requires additional records, be sure to add
the Zone or Record to the appropriate JSON file.
+41
View File
@@ -0,0 +1,41 @@
module github.com/doubleu-labs/coredns-netbox-plugin-dns
go 1.22.2
require (
github.com/coredns/caddy v1.1.1
github.com/coredns/coredns v1.11.3
github.com/miekg/dns v1.1.59
)
require (
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/onsi/ginkgo/v2 v2.17.2 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.14.0 // indirect
github.com/quic-go/quic-go v0.43.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.34.0 // indirect
)
+85
View File
@@ -0,0 +1,85 @@
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0=
github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4=
github.com/coredns/coredns v1.11.3 h1:8RjnpZc42db5th84/QJKH2i137ecJdzZK1HJwhetSPk=
github.com/coredns/coredns v1.11.3/go.mod h1:lqFkDsHjEUdY7LJ75Nib3lwqJGip6ewWOqNIf8OavIQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e h1:RsXNnXE59RTt8o3DcA+w7ICdRfR2l+Bb5aE0YMpNTO8=
github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
github.com/quic-go/quic-go v0.43.0 h1:sjtsTKWX0dsHpuMJvLxGqoQdtgJnbAPWY+W+5vjYW/g=
github.com/quic-go/quic-go v0.43.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+111
View File
@@ -0,0 +1,111 @@
package netbox
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
type APIRequestClient struct {
Client *http.Client
NetboxURL *url.URL
Token string
UserAgent string
}
type APIResultModel interface {
Record | Zone
}
type APIManyResponse[T APIResultModel] struct {
Count int `json:"count"`
Next string `json:"next"`
Previous string `json:"previous"`
Results []T `json:"results"`
}
func doGet(
requestClient *APIRequestClient,
url string,
) (*http.Response, error) {
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Set(
"Authorization",
fmt.Sprintf("Token %s", requestClient.Token),
)
request.Header.Set("User-Agent", requestClient.UserAgent)
return requestClient.Client.Do(request)
}
func responseError(response *http.Response) error {
if response.StatusCode != http.StatusOK {
return fmt.Errorf(
"request error [%d] %q",
response.StatusCode,
response.Status,
)
}
return nil
}
func get[T APIResultModel](
requestClient *APIRequestClient,
url string,
) (T, error) {
var out T
response, err := doGet(requestClient, url)
if err != nil {
return out, err
}
defer response.Body.Close()
if err := responseError(response); err != nil {
return out, err
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&out); err != nil {
return out, fmt.Errorf("could not unmarshal response: %w", err)
}
return out, nil
}
func getMany[T APIResultModel](
requestClient *APIRequestClient,
url string,
) ([]T, error) {
nextUrl := url
var out []T
for nextUrl != "" {
response, err := doGet(requestClient, nextUrl)
if err != nil {
return out, err
}
defer response.Body.Close()
if err := responseError(response); err != nil {
return out, err
}
var apiResponse APIManyResponse[T]
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&apiResponse); err != nil {
return out, fmt.Errorf("could not unmarshal response: %w", err)
}
if out == nil {
out = make([]T, 0, apiResponse.Count)
}
out = append(out, apiResponse.Results...)
nextUrl = apiResponse.Next
}
return out, nil
}
+99
View File
@@ -0,0 +1,99 @@
package netbox
import (
"net/url"
"strconv"
)
type Record struct {
Type string `json:"type"`
Value string `json:"value"`
TTL *uint32 `json:"ttl"`
Zone Zone `json:"zone"`
FQDN string `json:"fqdn"`
}
type RecordQuery struct {
FQDN string
Name string
Type []string
Zone *Zone
}
func (recordQuery *RecordQuery) Encode() string {
out := url.Values{}
if recordQuery.FQDN != "" {
out.Set("fqdn", recordQuery.FQDN)
}
if recordQuery.Name != "" {
out.Set("name", recordQuery.Name)
}
if len(recordQuery.Type) != 0 {
for _, t := range recordQuery.Type {
out.Add("type", t)
}
}
if recordQuery.Zone != nil {
out.Set("zone_id", strconv.Itoa(recordQuery.Zone.ID))
}
return out.Encode()
}
func urlRecords(netboxurl *url.URL) *url.URL {
return netboxurl.JoinPath("records", "/")
}
func GetRecordsQuery(
requestClient *APIRequestClient,
query *RecordQuery,
) ([]Record, error) {
requestUrl := urlRecords(requestClient.NetboxURL)
requestUrl.RawQuery = query.Encode()
records, err := getMany[Record](requestClient, requestUrl.String())
if err != nil {
return nil, err
}
if query.Zone != nil {
for k, record := range records {
if record.TTL == nil {
records[k].TTL = &query.Zone.DefaultTTL
}
}
} else {
resolvedRecords, err := resolveRecordTTLs(requestClient, records)
if err != nil {
return records, err
}
records = resolvedRecords
}
return records, nil
}
func resolveRecordTTLs(
requestClient *APIRequestClient,
records []Record,
) ([]Record, error) {
zoneTTL := make(map[int]uint32)
for k, record := range records {
if record.TTL != nil {
continue
}
if ttl, ok := zoneTTL[record.Zone.ID]; ok {
records[k].TTL = &ttl
continue
}
zoneUrl := urlZoneID(requestClient.NetboxURL, record.Zone.ID)
zone, err := get[Zone](requestClient, zoneUrl.String())
if err != nil {
return records, err
}
zoneTTL[zone.ID] = zone.DefaultTTL
records[k].TTL = &zone.DefaultTTL
}
return records, nil
}
+34
View File
@@ -0,0 +1,34 @@
package netbox
import (
"net/url"
"strconv"
)
type Zone struct {
DefaultTTL uint32 `json:"default_ttl"`
ID int `json:"id"`
Name string `json:"name"`
NameServers []SOAMName `json:"nameservers"`
}
type SOAMName struct {
Name string `json:"name"`
}
func urlZones(netboxurl *url.URL) *url.URL {
return netboxurl.JoinPath("zones", "/")
}
func urlZoneID(netboxurl *url.URL, id int) *url.URL {
return netboxurl.JoinPath("zones", "/", strconv.Itoa(id), "/")
}
func GetZones(requestClient *APIRequestClient) ([]Zone, error) {
requestUrl := urlZones(requestClient.NetboxURL)
zones, err := getMany[Zone](requestClient, requestUrl.String())
if err != nil {
return nil, err
}
return zones, nil
}
+273
View File
@@ -0,0 +1,273 @@
package netboxdns
import (
"strings"
"github.com/doubleu-labs/coredns-netbox-plugin-dns/internal/netbox"
"github.com/miekg/dns"
)
type lookupResult int
const (
lookupSuccess lookupResult = iota
lookupNameError // NXDomain
lookupDelegation // Delegate, non-authoritative
)
type lookupResponse struct {
Answer []dns.RR
Ns []dns.RR
Extra []dns.RR
LookupResult lookupResult
}
func (netboxdns *NetboxDNS) lookup(
name string,
qtype uint16,
family int,
) (*lookupResponse, error) {
nameTrimmed := strings.TrimSuffix(name, ".")
// check if zone exists on Netbox
zone, err := netboxdns.matchZone(nameTrimmed)
if err != nil {
return nil, err
}
if zone == nil {
return &lookupResponse{LookupResult: lookupNameError}, nil
}
// check if qname is for zone origin
if nameTrimmed == zone.Name {
originResponse, err := netboxdns.processOrigin(qtype, zone, family)
if err != nil {
return nil, err
}
if originResponse != nil {
return originResponse, nil
}
}
// lookup exact request
direct, err := netboxdns.lookupDirect(nameTrimmed, qtype, zone, family)
if err != nil {
return nil, err
}
if direct != nil {
return direct, nil
}
// if no exact records exist for the request, check if the qname is a
// delegate zone
delegate, err := netboxdns.lookupDelegate(nameTrimmed, zone, family)
if err != nil {
return nil, err
}
if delegate != nil {
return delegate, nil
}
return &lookupResponse{LookupResult: lookupNameError}, nil
}
func (netboxdns *NetboxDNS) matchZone(qname string) (*netbox.Zone, error) {
managedZones, err := netbox.GetZones(netboxdns.requestClient)
if err != nil {
return nil, err
}
var out *netbox.Zone
for _, managedZone := range managedZones {
if dns.IsSubDomain(managedZone.Name, qname) {
if out == nil {
out = &managedZone
}
if len(managedZone.Name) > len(out.Name) {
out = &managedZone
}
}
}
return out, nil
}
func (netboxdns *NetboxDNS) processOrigin(
qtype uint16,
zone *netbox.Zone,
family int,
) (*lookupResponse, error) {
var queryType []string
switch qtype {
case dns.TypeSOA:
queryType = []string{"SOA", "NS"}
case dns.TypeNS:
queryType = []string{"NS"}
default:
return nil, nil
}
records, err := netbox.GetRecordsQuery(
netboxdns.requestClient,
&netbox.RecordQuery{
Name: "@",
Type: queryType,
Zone: zone,
},
)
if err != nil {
return nil, err
}
rrs, err := recordsToRR(records)
if err != nil {
return nil, err
}
answer := filterRRByType(rrs, dns.TypeSOA)
ns := filterRRByType(rrs, dns.TypeNS)
extraRecords, err := netboxdns.processExtra(ns, zone, family)
if err != nil {
return nil, err
}
if len(extraRecords) == 0 {
// if no A/AAAA records exist for the NS in the specified zone, check if
// the server has records anywhere
extraRecords, err = netboxdns.processExtra(ns, nil, family)
if err != nil {
return nil, err
}
}
extra, err := recordsToRR(extraRecords)
if err != nil {
return nil, err
}
if qtype == dns.TypeNS {
answer = ns
ns = nil
}
return &lookupResponse{
Answer: answer,
Ns: ns,
Extra: extra,
}, nil
}
func (netboxdns *NetboxDNS) processExtra(
answer []dns.RR,
zone *netbox.Zone,
family int,
) ([]netbox.Record, error) {
var out []netbox.Record
for _, rr := range answer {
name := ""
switch t := rr.(type) {
case *dns.SRV:
name = t.Target
case *dns.MX:
name = t.Mx
case *dns.NS:
name = t.Ns
case *dns.CNAME:
name = t.Target
}
if len(name) == 0 {
continue
}
var reqType []string
switch family {
case 1:
reqType = []string{"A"}
case 2:
reqType = []string{"AAAA"}
}
records, err := netbox.GetRecordsQuery(
netboxdns.requestClient,
&netbox.RecordQuery{
FQDN: strings.TrimSuffix(name, "."),
Type: reqType,
Zone: zone,
},
)
if err != nil {
return out, err
}
out = append(out, records...)
}
return out, nil
}
func (netboxdns *NetboxDNS) lookupDirect(
qname string,
qtype uint16,
zone *netbox.Zone,
family int,
) (*lookupResponse, error) {
records, err := netbox.GetRecordsQuery(
netboxdns.requestClient,
&netbox.RecordQuery{
FQDN: qname,
Type: []string{dns.TypeToString[qtype]},
Zone: zone,
},
)
if err != nil {
return nil, err
}
if len(records) > 0 {
answer, err := recordsToRR(records)
if err != nil {
return nil, err
}
extraRecords, err := netboxdns.processExtra(answer, zone, family)
if err != nil {
return nil, err
}
extra, err := recordsToRR(extraRecords)
if err != nil {
return nil, err
}
if qtype == dns.TypeCNAME {
answer = append(answer, extra...)
extra = nil
}
return &lookupResponse{
Answer: answer,
Extra: extra,
}, nil
}
return nil, nil
}
func (netboxdns *NetboxDNS) lookupDelegate(
qname string,
zone *netbox.Zone,
family int,
) (*lookupResponse, error) {
records, err := netbox.GetRecordsQuery(
netboxdns.requestClient,
&netbox.RecordQuery{
FQDN: qname,
Type: []string{"NS"},
Zone: zone,
},
)
if err != nil {
return nil, err
}
if len(records) > 0 {
ns, err := recordsToRR(records)
if err != nil {
return nil, err
}
extraRecords, err := netboxdns.processExtra(ns, nil, family)
if err != nil {
return nil, err
}
extra, err := recordsToRR(extraRecords)
if err != nil {
return nil, err
}
return &lookupResponse{
Ns: ns,
Extra: extra,
LookupResult: lookupDelegation,
}, nil
}
return nil, nil
}
+109
View File
@@ -0,0 +1,109 @@
package netboxdns
import (
"context"
"net/http"
"time"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/request"
"github.com/doubleu-labs/coredns-netbox-plugin-dns/internal/netbox"
"github.com/miekg/dns"
)
const (
defaultHTTPClientTimeout time.Duration = time.Second * 5
pluginName string = "netboxdns"
)
var logger log.P
func init() {
logger = log.NewWithPlugin(pluginName)
}
type NetboxDNS struct {
Next plugin.Handler
requestClient *netbox.APIRequestClient
zones []string
fall fall.F
}
func NewNetboxDNS() *NetboxDNS {
return &NetboxDNS{
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
},
zones: []string{"."},
}
}
// Name implements the plugin.Handler interface
func (NetboxDNS) Name() string {
return pluginName
}
// ServeDNS implements the plugin.Handler interface
func (netboxdns *NetboxDNS) ServeDNS(
reqContext context.Context,
respWriter dns.ResponseWriter,
reqMsg *dns.Msg,
) (int, error) {
state := request.Request{W: respWriter, Req: reqMsg}
qname := state.QName()
qtype := state.QType()
// check if plugin is configured to respond to the requested zone
respondingZone := plugin.Zones(netboxdns.zones).Matches(qname)
if respondingZone == "" {
return netboxdns.nextOrFailure(reqContext, respWriter, reqMsg)
}
response, err := netboxdns.lookup(qname, qtype, state.Family())
if err != nil {
return dns.RcodeServerFailure, err
}
if response.LookupResult == lookupNameError &&
netboxdns.fall.Through(qname) {
return netboxdns.nextOrFailure(reqContext, respWriter, reqMsg)
}
respMsg := &dns.Msg{
Answer: response.Answer,
Ns: response.Ns,
Extra: response.Extra,
}
respMsg.SetReply(reqMsg)
respMsg.Authoritative = true
switch response.LookupResult {
case lookupSuccess:
case lookupNameError:
respMsg.Rcode = dns.RcodeNameError
case lookupDelegation:
respMsg.Authoritative = false
}
respWriter.WriteMsg(respMsg)
return dns.RcodeSuccess, nil
}
func (netboxdns *NetboxDNS) nextOrFailure(
ctx context.Context,
writer dns.ResponseWriter,
request *dns.Msg,
) (int, error) {
return plugin.NextOrFailure(
pluginName,
netboxdns.Next,
ctx,
writer,
request,
)
}
+788
View File
@@ -0,0 +1,788 @@
package netboxdns
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/doubleu-labs/coredns-netbox-plugin-dns/internal/netbox"
"github.com/miekg/dns"
)
func TestPluginName(t *testing.T) {
netboxdns := &NetboxDNS{}
name := netboxdns.Name()
if name != pluginName {
t.Errorf("plugin name is wrong. how did this happen...")
}
}
type testFamily int
const (
testFamilyV4 testFamily = iota
testFamilyV6
)
var testFamilyToString map[testFamily]string = map[testFamily]string{
testFamilyV4: "V4",
testFamilyV6: "V6",
}
const (
testInstanceToken string = "w5pgWXPqZVmngLN4w4XwuPvZfUC72ytDxnnHgEmI"
testInstanceUrlHost string = "localhost:9999"
testInstanceUrlPath string = "/api/plugins/netbox-dns/"
)
var netboxdnsPlugin NetboxDNS = NetboxDNS{
Next: test.ErrorHandler(),
zones: []string{"."},
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
NetboxURL: &url.URL{
Scheme: "http",
Host: testInstanceUrlHost,
Path: testInstanceUrlPath,
},
Token: testInstanceToken,
},
}
func RunTestLookup(t *testing.T, tcs []test.Case, family testFamily) {
for _, tc := range tcs {
tcName := fmt.Sprintf(
"%s %s %s",
tc.Qname,
dns.TypeToString[tc.Qtype],
testFamilyToString[family],
)
t.Run(tcName, func(t *testing.T) {
msg := tc.Msg()
respWriter := GetTestResponseWriter(family)
rec := dnstest.NewRecorder(respWriter)
_, err := netboxdnsPlugin.ServeDNS(context.Background(), rec, msg)
if err != nil {
t.Errorf("expected no error, got %v", err)
return
}
resp := rec.Msg
if resp == nil {
t.Fatal("got nil response message")
}
if ok := RunTestLookupCheckCNAME(t, tc, resp); !ok {
return
}
if err := test.SortAndCheck(resp, tc); err != nil {
t.Logf("%s\n", rec.Msg)
t.Error(err)
}
})
}
}
func GetTestResponseWriter(family testFamily) dns.ResponseWriter {
var respWriter dns.ResponseWriter
switch family {
case testFamilyV4:
respWriter = &test.ResponseWriter{}
case testFamilyV6:
respWriter = &test.ResponseWriter6{}
}
return respWriter
}
func RunTestLookupCheckCNAME(t *testing.T, tc test.Case, resp *dns.Msg) bool {
if err := test.CNAMEOrder(resp); err != nil {
t.Errorf("cname response out of order")
}
if tc.Qtype == dns.TypeCNAME {
if err := test.Header(tc, resp); err != nil {
t.Error(err)
}
if err := test.Section(tc, test.Answer, resp.Answer); err != nil {
t.Error(err)
}
if err := test.Section(tc, test.Ns, resp.Ns); err != nil {
t.Error(err)
}
if err := test.Section(tc, test.Extra, resp.Extra); err != nil {
t.Error(err)
}
return false
}
return true
}
var (
exampledotcomName string = "example.com."
subdotexampledotcomName string = "sub.example.com."
subtwodotexampledotcomName string = "subtwo.example.com."
exampledotcomNS []dns.RR = []dns.RR{
test.NS("example.com. 3600 IN NS dns01.example.com."),
test.NS("example.com. 3600 IN NS dns02.example.com."),
}
subdotexampledotcomNS []dns.RR = []dns.RR{
test.NS("sub.example.com. 3600 IN NS dns01.example.com."),
test.NS("sub.example.com. 3600 IN NS dns02.example.com."),
}
subtwodotexampledotcomNS []dns.RR = []dns.RR{
test.NS("subtwo.example.com. 3600 IN NS dns01.example.com."),
test.NS("subtwo.example.com. 3600 IN NS dns02.example.com."),
}
exampledotcomNS1Record4 dns.RR = test.A("dns01.example.com. 3600 IN A 10.0.0.10")
exampledotcomNS2Record4 dns.RR = test.A("dns02.example.com. 3600 IN A 10.0.0.11")
exampledotcomNSAddr4 []dns.RR = []dns.RR{
exampledotcomNS1Record4,
exampledotcomNS2Record4,
}
exampledotcomNS1Record6 dns.RR = test.AAAA("dns01.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:10")
exampledotcomNS2Record6 dns.RR = test.AAAA("dns02.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:11")
exampledotcomNSAddr6 []dns.RR = []dns.RR{
exampledotcomNS1Record6,
exampledotcomNS2Record6,
}
)
var (
testLookupForwardZonesCasesv4 []test.Case = []test.Case{
{
Qname: exampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: exampledotcomNS,
Extra: exampledotcomNSAddr4,
},
{
Qname: subdotexampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("sub.example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("sub.example.com. 3600 IN NS dns01.example.com"),
test.NS("sub.example.com. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr4,
},
{
Qname: subtwodotexampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("subtwo.example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("subtwo.example.com. 3600 IN NS dns01.example.com"),
test.NS("subtwo.example.com. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr4,
},
}
testLookupReverseZonesCasesv4 []test.Case = []test.Case{
{
Qname: "0.0.10.in-addr.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("0.0.10.in-addr.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("0.0.10.in-addr.arpa. 3600 IN NS dns01.example.com"),
test.NS("0.0.10.in-addr.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr4,
},
{
Qname: "1.0.10.in-addr.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("1.0.10.in-addr.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("1.0.10.in-addr.arpa. 3600 IN NS dns01.example.com"),
test.NS("1.0.10.in-addr.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr4,
},
{
Qname: "2.0.10.in-addr.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("2.0.10.in-addr.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("2.0.10.in-addr.arpa. 3600 IN NS dns01.example.com"),
test.NS("2.0.10.in-addr.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr4,
},
}
testLookupForwardZonesCasesv6 []test.Case = []test.Case{
{
Qname: exampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: exampledotcomNS,
Extra: exampledotcomNSAddr6,
},
{
Qname: subdotexampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("sub.example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("sub.example.com. 3600 IN NS dns01.example.com"),
test.NS("sub.example.com. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr6,
},
{
Qname: subtwodotexampledotcomName, Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("subtwo.example.com. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("subtwo.example.com. 3600 IN NS dns01.example.com"),
test.NS("subtwo.example.com. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr6,
},
}
testLookupReverseZonesCasesv6 []test.Case = []test.Case{
{
Qname: "1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns01.example.com"),
test.NS("1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr6,
},
{
Qname: "2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns01.example.com"),
test.NS("2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr6,
},
{
Qname: "3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
test.SOA("3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 86400 IN SOA dns01.example.com. admin.example.com. 1 43200 7200 2419200 3600"),
},
Ns: []dns.RR{
test.NS("3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns01.example.com"),
test.NS("3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN NS dns02.example.com"),
},
Extra: exampledotcomNSAddr6,
},
}
)
func TestLookupZones(t *testing.T) {
RunTestLookup(t, testLookupForwardZonesCasesv4, testFamilyV4)
RunTestLookup(t, testLookupReverseZonesCasesv4, testFamilyV4)
RunTestLookup(t, testLookupForwardZonesCasesv6, testFamilyV6)
RunTestLookup(t, testLookupReverseZonesCasesv6, testFamilyV6)
}
var (
testLookupRecordV4 []test.Case = []test.Case{
{
Qname: exampledotcomName, Qtype: dns.TypeNS,
Answer: exampledotcomNS,
Extra: exampledotcomNSAddr4,
},
{
Qname: "dns01.example.com", Qtype: dns.TypeA,
Answer: []dns.RR{
exampledotcomNS1Record4,
},
},
{
Qname: "dns02.example.com", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("dns02.example.com. 3600 IN A 10.0.0.11"),
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeA,
Ns: exampledotcomNS,
Extra: exampledotcomNSAddr4,
},
{
Qname: "aservice.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("aservice.example.com. 3600 IN A 10.0.0.12"),
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("example.com. 3600 IN MX 10 mail.example.com."),
},
Extra: []dns.RR{
test.A("mail.example.com. 3600 IN A 10.0.0.13"),
},
},
{
Qname: "mail.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("mail.example.com. 3600 IN A 10.0.0.13"),
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeTXT,
Answer: []dns.RR{
test.TXT(`example.com. 3600 IN TXT "my value" "second my value" "third my value"`),
test.TXT(`example.com. 3600 IN TXT "newline record" "second value"`),
test.TXT(`example.com. 3600 IN TXT "some value" "another value"`),
test.TXT(`example.com. 3600 IN TXT "v=DMARC1;p=none;sp=quarantine;pct=100;rua=admin@example.com;"`),
test.TXT(`example.com. 3600 IN TXT "v=spf1 ip4:10.0.0.13 ip6:2001:db8:dead:beef::1:13 a -all"`),
},
},
{
Qname: "puppet-server-a.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("puppet-server-a.example.com. 3600 IN A 10.0.0.15"),
},
},
{
Qname: "puppet-server-b.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("puppet-server-b.example.com. 3600 IN A 10.0.0.16"),
},
},
{
Qname: "_x-puppet._tcp.example.com.", Qtype: dns.TypeSRV,
Answer: []dns.RR{
test.SRV("_x-puppet._tcp.example.com. 3600 IN SRV 0 5 8140 puppet-server-a.example.com."),
test.SRV("_x-puppet._tcp.example.com. 3600 IN SRV 0 5 8140 puppet-server-b.example.com."),
},
Extra: []dns.RR{
test.A("puppet-server-a.example.com. 3600 IN A 10.0.0.15"),
test.A("puppet-server-b.example.com. 3600 IN A 10.0.0.16"),
},
},
{
Qname: "web.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("web.example.com. 3600 IN A 10.0.0.17"),
},
},
{
Qname: "www.example.com.", Qtype: dns.TypeCNAME,
Answer: []dns.RR{
test.CNAME("www.example.com. 3600 IN CNAME web.example.com."),
test.A("web.example.com. 3600 IN A 10.0.0.17"),
},
},
{
Qname: subdotexampledotcomName, Qtype: dns.TypeNS,
Answer: subdotexampledotcomNS,
Extra: exampledotcomNSAddr4,
},
{
Qname: "myservice.sub.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("myservice.sub.example.com. 3600 IN A 10.0.1.10"),
},
},
{
Qname: subtwodotexampledotcomName, Qtype: dns.TypeNS,
Answer: subtwodotexampledotcomNS,
Extra: exampledotcomNSAddr4,
},
{
Qname: "myotherservice.subtwo.example.com.", Qtype: dns.TypeA,
Answer: []dns.RR{
test.A("myotherservice.subtwo.example.com. 3600 IN A 10.0.2.10"),
},
},
}
testLookupPTRV4 []test.Case = []test.Case{
{
Qname: "10.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("10.0.0.10.in-addr.arpa. 3600 IN PTR dns01.example.com."),
},
},
{
Qname: "11.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("11.0.0.10.in-addr.arpa. 3600 IN PTR dns02.example.com."),
},
},
{
Qname: "12.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("12.0.0.10.in-addr.arpa. 3600 IN PTR aservice.example.com."),
},
},
{
Qname: "13.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("13.0.0.10.in-addr.arpa. 3600 IN PTR mail.example.com."),
},
},
{
Qname: "15.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("15.0.0.10.in-addr.arpa. 3600 IN PTR puppet-server-a.example.com."),
},
},
{
Qname: "16.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("16.0.0.10.in-addr.arpa. 3600 IN PTR puppet-server-b.example.com."),
},
},
{
Qname: "17.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("17.0.0.10.in-addr.arpa. 3600 IN PTR web.example.com."),
},
},
{
Qname: "10.1.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("10.1.0.10.in-addr.arpa. 3600 IN PTR myservice.sub.example.com."),
},
},
{
Qname: "10.2.0.10.in-addr.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("10.2.0.10.in-addr.arpa. 3600 IN PTR myotherservice.subtwo.example.com."),
},
},
}
testLookupRecordV6 []test.Case = []test.Case{
{
Qname: exampledotcomName, Qtype: dns.TypeNS,
Answer: exampledotcomNS,
Extra: exampledotcomNSAddr6,
},
{
Qname: "dns01.example.com", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
exampledotcomNS1Record6,
},
},
{
Qname: "dns02.example.com", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
exampledotcomNS2Record6,
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeAAAA,
Ns: exampledotcomNS,
Extra: exampledotcomNSAddr6,
},
{
Qname: "aservice.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("aservice.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:12"),
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("example.com. 3600 IN MX 10 mail.example.com."),
},
Extra: []dns.RR{
test.AAAA("mail.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:13"),
},
},
{
Qname: "mail.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("mail.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:13"),
},
},
{
Qname: exampledotcomName, Qtype: dns.TypeTXT,
Answer: []dns.RR{
test.TXT(`example.com. 3600 IN TXT "my value" "second my value" "third my value"`),
test.TXT(`example.com. 3600 IN TXT "newline record" "second value"`),
test.TXT(`example.com. 3600 IN TXT "some value" "another value"`),
test.TXT(`example.com. 3600 IN TXT "v=DMARC1;p=none;sp=quarantine;pct=100;rua=admin@example.com;"`),
test.TXT(`example.com. 3600 IN TXT "v=spf1 ip4:10.0.0.13 ip6:2001:db8:dead:beef::1:13 a -all"`),
},
},
{
Qname: "puppet-server-a.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("puppet-server-a.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:15"),
},
},
{
Qname: "puppet-server-b.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("puppet-server-b.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:16"),
},
},
{
Qname: "_x-puppet._tcp.example.com.", Qtype: dns.TypeSRV,
Answer: []dns.RR{
test.SRV("_x-puppet._tcp.example.com. 3600 IN SRV 0 5 8140 puppet-server-a.example.com."),
test.SRV("_x-puppet._tcp.example.com. 3600 IN SRV 0 5 8140 puppet-server-b.example.com."),
},
Extra: []dns.RR{
test.AAAA("puppet-server-a.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:15"),
test.AAAA("puppet-server-b.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:16"),
},
},
{
Qname: "web.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("web.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:17"),
},
},
{
Qname: "www.example.com.", Qtype: dns.TypeCNAME,
Answer: []dns.RR{
test.CNAME("www.example.com. 3600 IN CNAME web.example.com."),
test.AAAA("web.example.com. 3600 IN AAAA 2001:db8:dead:beef::1:17"),
},
},
{
Qname: subdotexampledotcomName, Qtype: dns.TypeNS,
Answer: subdotexampledotcomNS,
Extra: exampledotcomNSAddr6,
},
{
Qname: "myservice.sub.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("myservice.sub.example.com. 3600 IN AAAA 2001:db8:dead:beef::2:10"),
},
},
{
Qname: subtwodotexampledotcomName, Qtype: dns.TypeNS,
Answer: subtwodotexampledotcomNS,
Extra: exampledotcomNSAddr6,
},
{
Qname: "myotherservice.subtwo.example.com.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
test.AAAA("myotherservice.subtwo.example.com. 3600 IN AAAA 2001:db8:dead:beef::3:10"),
},
},
}
testLookupPTRV6 []test.Case = []test.Case{
{
Qname: "0.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("0.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR dns01.example.com."),
},
},
{
Qname: "1.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("1.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR dns02.example.com."),
},
},
{
Qname: "2.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("2.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR aservice.example.com."),
},
},
{
Qname: "3.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("3.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR mail.example.com."),
},
},
{
Qname: "5.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("5.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR puppet-server-a.example.com."),
},
},
{
Qname: "6.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("6.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR puppet-server-b.example.com."),
},
},
{
Qname: "7.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("7.1.0.0.1.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR web.example.com."),
},
},
{
Qname: "0.1.0.0.2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("0.1.0.0.2.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR myservice.sub.example.com."),
},
},
{
Qname: "0.1.0.0.3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa.", Qtype: dns.TypePTR,
Answer: []dns.RR{
test.PTR("0.1.0.0.3.0.0.0.0.0.0.0.0.0.0.0.f.e.e.b.d.a.e.d.8.b.d.0.1.0.0.2.ip6.arpa. 3600 IN PTR myotherservice.subtwo.example.com."),
},
},
}
)
func TestLookupRecords(t *testing.T) {
RunTestLookup(t, testLookupRecordV4, testFamilyV4)
RunTestLookup(t, testLookupPTRV4, testFamilyV4)
RunTestLookup(t, testLookupRecordV6, testFamilyV6)
RunTestLookup(t, testLookupPTRV6, testFamilyV6)
}
var (
testUnknownRecords []test.Case = []test.Case{
{
Qname: "noop.com.", Qtype: dns.TypeSOA,
Rcode: dns.RcodeNameError,
},
}
testUnknownRecordsV4 []test.Case = []test.Case{
{
Qname: "noop.example.com.", Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
},
}
testUnknownRecordsV6 []test.Case = []test.Case{
{
Qname: "noop.example.com.", Qtype: dns.TypeAAAA,
Rcode: dns.RcodeNameError,
},
}
)
func TestLookupUnknown(t *testing.T) {
RunTestLookup(t, testUnknownRecords, testFamilyV4)
RunTestLookup(t, testUnknownRecordsV4, testFamilyV4)
RunTestLookup(t, testUnknownRecordsV6, testFamilyV6)
}
func TestOffline(t *testing.T) {
netboxdns := NetboxDNS{
Next: test.ErrorHandler(),
zones: []string{"."},
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
NetboxURL: &url.URL{
Scheme: "http",
Host: "localhost:9876",
Path: testInstanceUrlPath,
},
Token: testInstanceToken,
},
}
tc := test.Case{
Qname: exampledotcomName, Qtype: dns.TypeA,
}
msg := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := netboxdns.ServeDNS(context.Background(), rec, msg)
if err == nil {
t.Error("expected connection error, got none")
}
}
func TestUnauthorized(t *testing.T) {
netboxdns := NetboxDNS{
Next: test.ErrorHandler(),
zones: []string{"."},
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
NetboxURL: &url.URL{
Scheme: "http",
Host: testInstanceUrlHost,
Path: testInstanceUrlPath,
},
Token: "noop",
},
}
tc := test.Case{
Qname: exampledotcomName, Qtype: dns.TypeA,
}
msg := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := netboxdns.ServeDNS(context.Background(), rec, msg)
if err == nil {
t.Error("expected connection error, got none")
}
}
func TestFallthrough(t *testing.T) {
netboxdns := NetboxDNS{
Next: test.ErrorHandler(),
zones: []string{exampledotcomName},
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
NetboxURL: &url.URL{
Scheme: "http",
Host: testInstanceUrlHost,
Path: testInstanceUrlPath,
},
Token: testInstanceToken,
},
}
netboxdns.fall.SetZonesFromArgs([]string{"out.example.com"})
tc := test.Case{
Qname: "a.out.example.com.", Qtype: dns.TypeA,
}
msg := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := netboxdns.ServeDNS(context.Background(), rec, msg)
if err != nil {
t.Errorf("expected fallthrough, got %v", err)
}
}
func TestUnhandledZone(t *testing.T) {
netboxdns := NetboxDNS{
Next: test.ErrorHandler(),
zones: []string{exampledotcomName},
requestClient: &netbox.APIRequestClient{
Client: &http.Client{
Timeout: defaultHTTPClientTimeout,
},
NetboxURL: &url.URL{
Scheme: "http",
Host: testInstanceUrlHost,
Path: testInstanceUrlPath,
},
Token: testInstanceToken,
},
}
tc := test.Case{
Qname: "www.example.net.", Qtype: dns.TypeA,
}
msg := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := netboxdns.ServeDNS(context.Background(), rec, msg)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}
+179
View File
@@ -0,0 +1,179 @@
package netboxdns
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/tls"
)
type tokenFuncMap map[string]func(*caddy.Controller, *NetboxDNS) error
var tokenFuncs tokenFuncMap
func init() {
tokenFuncs = tokenFuncMap{
"fallthrough": parseFallthrough,
"timeout": parseTimeout,
"tls": parseTLS,
"token": parseToken,
"url": parseUrl,
}
}
// Parse netboxdns configuration
func Parse(controller *caddy.Controller, netboxdns *NetboxDNS) error {
instances := 0
for controller.Next() {
if instances > 0 {
return plugin.ErrOnce
}
instances++
parseZones(controller, netboxdns)
if err := parseConfigTokens(controller, netboxdns); err != nil {
return err
}
}
if err := parseValidate(controller, netboxdns); err != nil {
return err
}
fullPluginURL := netboxdns.requestClient.NetboxURL.JoinPath(
"api",
"plugins",
"netbox-dns",
)
netboxdns.requestClient.NetboxURL = fullPluginURL
netboxdns.requestClient.UserAgent = fmt.Sprintf(
"coredns plugin %s",
pluginName,
)
return nil
}
func parseZones(controller *caddy.Controller, netboxdns *NetboxDNS) {
zones := plugin.OriginsFromArgsOrServerBlock(
controller.RemainingArgs(),
controller.ServerBlockKeys,
)
if len(zones) > 0 {
netboxdns.zones = zones
}
}
func parseConfigTokens(controller *caddy.Controller, netboxdns *NetboxDNS) error {
for controller.NextBlock() {
tokenName := controller.Val()
tokenFunc, ok := tokenFuncs[tokenName]
if !ok {
return unknownToken(controller, tokenName)
}
if err := tokenFunc(controller, netboxdns); err != nil {
return err
}
}
return nil
}
func unknownToken(controller *caddy.Controller, unknownToken string) error {
expectedTokenString := ""
i := 0
for tokenName := range tokenFuncs {
expectedTokenString += fmt.Sprintf("%q", tokenName)
if i+1 < len(tokenFuncs) {
expectedTokenString += ", "
}
if i == len(tokenFuncs)-2 {
expectedTokenString += "or "
}
i++
}
return controller.Errf(
"unknown token %q; expected %s",
unknownToken,
expectedTokenString,
)
}
func parseFallthrough(
controller *caddy.Controller,
netboxdns *NetboxDNS,
) error {
netboxdns.fall.SetZonesFromArgs(controller.RemainingArgs())
return nil
}
func parseTimeout(controller *caddy.Controller, netboxdns *NetboxDNS) error {
if !controller.NextArg() {
return controller.Err(`no value for "timeout" provided`)
}
duration, err := time.ParseDuration(controller.Val())
if err != nil {
return controller.Errf(
`there was an error parsing "timeout": %q`,
err.Error(),
)
}
netboxdns.requestClient.Client.Timeout = duration
return nil
}
func parseTLS(controller *caddy.Controller, netboxdns *NetboxDNS) error {
args := controller.RemainingArgs()
tlsConfig, err := tls.NewTLSConfigFromArgs(args...)
if err != nil {
return err
}
netboxdns.requestClient.Client.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
return nil
}
func parseToken(controller *caddy.Controller, netboxdns *NetboxDNS) error {
if !controller.NextArg() {
return controller.Err(`no value for "token" provided`)
}
netboxdns.requestClient.Token = controller.Val()
return nil
}
func parseUrl(controller *caddy.Controller, netboxdns *NetboxDNS) error {
if !controller.NextArg() {
return controller.Err(`no value for "url" provided`)
}
netboxUrl, err := url.Parse(controller.Val())
if err != nil {
return controller.Errf(
`there was an error parsing "url": %q`,
err.Error(),
)
}
netboxdns.requestClient.NetboxURL = netboxUrl
return nil
}
func parseValidate(controller *caddy.Controller, netboxdns *NetboxDNS) error {
tokenEmpty := netboxdns.requestClient.Token == ""
urlEmpty := netboxdns.requestClient.NetboxURL == nil ||
netboxdns.requestClient.NetboxURL.Host == ""
if tokenEmpty && urlEmpty {
return controller.Err(
`values are required for "token" and "url"`,
)
}
if tokenEmpty {
return controller.Err(`value is required for "token"`)
}
if urlEmpty {
return controller.Err(`value is required for "url"`)
}
return nil
}
+78
View File
@@ -0,0 +1,78 @@
package netboxdns
import (
"fmt"
"regexp"
"strings"
"github.com/doubleu-labs/coredns-netbox-plugin-dns/internal/netbox"
"github.com/miekg/dns"
)
var txtMultiValueRegexp *regexp.Regexp
func init() {
txtMultiValueRegexp = regexp.MustCompile(`[^\s"']+|"([^"]*)"|'([^']*)`)
}
func recordsToRR(records []netbox.Record) ([]dns.RR, error) {
out := make([]dns.RR, 0, len(records))
for _, record := range records {
qtype := dns.StringToType[record.Type]
switch qtype {
case dns.TypeTXT:
out = append(out, recordToTXT(record))
default:
rrStr := fmt.Sprintf(
"%s %d IN %s %s",
record.FQDN,
*record.TTL,
record.Type,
record.Value,
)
rr, err := dns.NewRR(rrStr)
if err != nil {
return out, err
}
out = append(out, rr)
}
}
return out, nil
}
func recordToTXT(record netbox.Record) *dns.TXT {
txt := make([]string, 0)
if strings.HasPrefix(record.Value, `"`) {
values := txtMultiValueRegexp.FindAllString(record.Value, -1)
for i := range values {
values[i] = strings.Trim(values[i], `"`)
values[i] = strings.ReplaceAll(values[i], "\\r\\n", "")
values[i] = strings.ReplaceAll(values[i], "\\n", "")
values[i] = strings.TrimSpace(values[i])
if values[i] != "" {
txt = append(txt, values[i])
}
}
} else {
txt = append(txt, record.Value)
}
return &dns.TXT{
Hdr: dns.RR_Header{
Name: record.FQDN,
Ttl: *record.TTL,
Class: dns.ClassINET,
Rrtype: dns.TypeTXT,
},
Txt: txt,
}
}
func filterRRByType(rrs []dns.RR, recordType uint16) []dns.RR {
out := make([]dns.RR, 0)
for _, rr := range rrs {
if rr.Header().Rrtype == recordType {
out = append(out, rr)
}
}
return out
}
+26
View File
@@ -0,0 +1,26 @@
package netboxdns
import (
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
)
func init() {
plugin.Register(pluginName, setup)
}
func setup(controller *caddy.Controller) error {
netboxdns := NewNetboxDNS()
if err := Parse(controller, netboxdns); err != nil {
return err
}
dnsserver.GetConfig(controller).AddPlugin(
func(next plugin.Handler) plugin.Handler {
netboxdns.Next = next
return netboxdns
},
)
logger.Info("successfully started netboxdns")
return nil
}
+199
View File
@@ -0,0 +1,199 @@
package netboxdns
import (
"testing"
"github.com/coredns/caddy"
)
type SetupTest struct {
Name string
Corefile string
WantErr bool
}
var setupTests []SetupTest = []SetupTest{
{
"no configuration",
`netboxdns`,
true,
},
{
"unknown token",
`netboxdns {
noop
}`,
true,
},
{
"no netbox token specified",
`netboxdns {
url http://localhost:9999/
}`,
true,
},
{
"no value for netbox token",
`netboxdns {
url http://localhost:9999/
token
}`,
true,
},
{
"no netbox url specified",
`netboxdns {
token sometoken
}`,
true,
},
{
"no value for netbox url",
`netboxdns {
token sometoken
url
}`,
true,
},
{
"minimum valid configuration",
`netboxdns {
token sometoken
url http://localhost:9999/
}`,
false,
},
{
"invalid netbox url value",
`netboxdns {
token sometoken
url "http://local host:9999/"
}`,
true,
},
{
"multiple configurations",
`netboxdns {
token sometoken
url http://localhost:9999/
}
netboxdns {
token sometoken
url http://localhost:9999/
}`,
true,
},
{
"configuration with responsible zone",
`netboxdns example.com {
token sometoken
url http://localhost:9999/
}`,
false,
},
{
"no value for timeout specified",
`netboxdns {
token sometoken
url http://localhost:9999/
timeout
}`,
true,
},
{
"minimum configuration with timeout",
`netboxdns {
token sometoken
url http://localhost:9999/
timeout 10s
}`,
false,
},
{
"invalid timeout",
`netboxdns {
token sometoken
url http://localhost:9999/
timeout 10g
}`,
true,
},
{
"minimum configuration fallthrough all zones",
`netboxdns {
token sometoken
url http://localhost:9999/
fallthrough
}`,
false,
},
{
"minimum configuration fallthrough specified zone",
`netboxdns {
token sometoken
url http://localhost:9999/
fallthrough example.net
}`,
false,
},
{
"minimum configuration tls system ca",
`netboxdns {
token sometoken
url http://localhost:9999/
tls
}`,
false,
},
{
"minimum configuration tls nonexistant file",
`netboxdns {
token sometoken
url http://localhost:9999/
tls noop.pem
}`,
true,
},
{
"minimum configuration tls private ca",
`netboxdns {
token sometoken
url http://localhost:9999/
tls .testing/tls/ca.pem
}`,
false,
},
{
"minimum configuration tls client auth",
`netboxdns {
token sometoken
url http://localhost:9999/
tls .testing/tls/client.pem .testing/tls/client-key.pem
}`,
false,
},
{
"minimum configuration tls client auth private ca",
`netboxdns {
token sometoken
url http://localhost:9999/
tls .testing/tls/client.pem .testing/tls/client-key.pem .testing/tls/ca.pem
}`,
false,
},
}
func TestSetup(t *testing.T) {
for _, tt := range setupTests {
t.Run(tt.Name, func(t *testing.T) {
controller := caddy.NewTestController("dns", tt.Corefile)
if err := setup(controller); (err != nil) != tt.WantErr {
t.Errorf(
"setup error: %v, wanterr: %t",
err,
tt.WantErr,
)
}
})
}
}