initial commit
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
coverage.out
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
PLUGINS = [
|
||||
'netbox_dns',
|
||||
]
|
||||
|
||||
PLUGINS_CONFIG = {
|
||||
'netbox_dns': {
|
||||
'feature_ipam_coupling': True,
|
||||
'tolerate_underscores_in_hostnames': True,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
Vendored
+34
@@ -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
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
POSTGRES_DB=netbox
|
||||
POSTGRES_PASSWORD=J5brHrAXFLQSif0K
|
||||
POSTGRES_USER=netbox
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=t4Ph722qJ5QHeQ1qfu36
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
REDIS_PASSWORD=H733Kdjndks81
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"name": "dns01.example.com"
|
||||
},
|
||||
{
|
||||
"name": "dns02.example.com"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
netbox-plugin-dns >= 0.22.8
|
||||
@@ -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
|
||||
@@ -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-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEILxY99DUkfyC9uFgkLzJoce2BkEwxI2FiBttKptbOFgBoAoGCCqGSM49
|
||||
AwEHoUQDQgAEM1w4sKz9to1SpdZ5whJK41t5JVAYivmFklD87IAQOKXqt5DKAX9r
|
||||
Z8f/95FVt8qGOYkG4OYP4sCfi8g2pnd6Jg==
|
||||
-----END EC PRIVATE KEY-----
|
||||
@@ -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-----
|
||||
Vendored
+15
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"go.testFlags": ["-v"]
|
||||
}
|
||||
Vendored
+80
@@ -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": ".*"
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,149 @@
|
||||
# netboxdns
|
||||
|
||||
[](https://pkg.go.dev/github.com/doubleu-labs/coredns-netbox-plugin-dns)
|
||||
[](https://sonarcloud.io/summary/overall?id=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.
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user