Skip to content

Commit

Permalink
Merge pull request #36 from kotso/master
Browse files Browse the repository at this point in the history
Implements Reverse DNS lookup based on netbox
  • Loading branch information
oz123 authored Mar 6, 2023
2 parents 61e272e + ccb2904 commit 289411e
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.51.2


# Optional: working directory, useful for monorepos
# working-directory: somedir

Expand Down
23 changes: 23 additions & 0 deletions examples/Corefile-reverse-zone-authority
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# cache, if entry is found it will be saved in the cache and not be
# asked again

. {
debug
template IN SOA in-addr.arpa {
answer "{{ .Name }} 86400 IN SOA ns1.example.com. sysmasters.example.com. (20230215 3600 600 86400 3600)"
authority "{{ .Name }} 86400 IN NS ns1.example.com."
authority "{{ .Name }} 86400 IN NS ns2.example.com."
fallthrough
}

netbox {
token 0123456789abcdef0123456789abcdef01234567
url http://localhost:8000/api/ipam/ip-addresses
fallthrough
}

cache 300 # see docs for the cache Plugin about config
}

# To make coredns authoritative for reverse zone you need to have soa record provided by template plugin
# and netbox plugin handled ptr record resolution via netbox api
26 changes: 22 additions & 4 deletions netbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ const (
// ServeDNS implements the plugin.Handler interface
func (n *Netbox) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var (
ips []net.IP
err error
ips []net.IP
domains []string
err error
)

state := request.Request{W: w, Req: r}
Expand All @@ -66,8 +67,8 @@ func (n *Netbox) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)

qname := state.Name()

// check record type here and bail out if not A or AAAA
if state.QType() != dns.TypeA && state.QType() != dns.TypeAAAA {
// check record type here and bail out if not A, AAAA or PTR
if state.QType() != dns.TypeA && state.QType() != dns.TypeAAAA && state.QType() != dns.TypePTR {
// always fallthrough if configured
if n.Fall.Through(qname) {
return plugin.NextOrFailure(n.Name(), n.Next, ctx, w, r)
Expand All @@ -91,6 +92,9 @@ func (n *Netbox) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
case dns.TypeAAAA:
ips, err = n.query(strings.TrimRight(qname, "."), familyIP6)
answers = aaaa(qname, uint32(n.TTL), ips)
case dns.TypePTR:
domains, err = n.queryreverse(qname)
answers = ptr(qname, uint32(n.TTL), domains)
}

if len(answers) == 0 {
Expand Down Expand Up @@ -148,6 +152,20 @@ func aaaa(zone string, ttl uint32, ips []net.IP) []dns.RR {
return answers
}

// ptr takes a slice of strings and returns a slice of PTR RRs.
func ptr(zone string, ttl uint32, domains []string) []dns.RR {

answers := make([]dns.RR, len(domains))
for i, domain := range domains {
r := new(dns.PTR)
r.Hdr = dns.RR_Header{Name: zone, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: ttl}
r.Ptr = domain
answers[i] = r
}

return answers
}

// dnserror writes a DNS error response back to the client. Based on plugin.BackendError
func dnserror(rcode int, state request.Request, err error) (int, error) {
m := new(dns.Msg)
Expand Down
34 changes: 34 additions & 0 deletions netbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
var hostWithIPv4 = `{"results": [{"family": {"value": 4, "label": "IPv4"},
"address": "10.0.0.2/25", "dns_name": "my_host"}]}`

var reverseDNS = `{"results": [{ "address": "10.0.0.2", "dns_name": "domain.com"}]}`

func TestNetbox(t *testing.T) {
defer gock.Off() // Flush pending mocks after test execution
gock.New("https://example.org/api/ipam/ip-addresses/").MatchParams(
Expand Down Expand Up @@ -58,3 +60,35 @@ func TestNetbox(t *testing.T) {
}

}

func TestReverseNetbox(t *testing.T) {
defer gock.Off() // Flush pending mocks after test execution
gock.New("https://example.org/api/ipam/ip-addresses/").MatchParams(
map[string]string{"address": "10.0.0.2"}).Reply(
200).BodyString(reverseDNS)
nb := newNetbox()
nb.Url = "https://example.org/api/ipam/ip-addresses"
nb.Token = "s3kr3tt0ken"

if nb.Name() != "netbox" {
t.Errorf("expected plugin name: %s, got %s", "netbox", nb.Name())
}

rec := dnstest.NewRecorder(&test.ResponseWriter{})
r := new(dns.Msg)
r.SetQuestion("2.0.0.10.in-addr.arpa.", dns.TypePTR)

rcode, err := nb.ServeDNS(context.Background(), rec, r)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if rcode != 0 {
t.Errorf("Expected rcode %v, got %v", 0, rcode)
}
Domain := rec.Msg.Answer[0].(*dns.PTR).Ptr

if Domain != "domain.com." {
t.Errorf("Expected %v, got %v", "domain.com.", Domain)
}

}
45 changes: 45 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"net"
"net/http"
"strings"

"github.com/coredns/coredns/plugin/pkg/dnsutil"
)

type Record struct {
Expand Down Expand Up @@ -101,3 +103,46 @@ func (n *Netbox) query(host string, family int) ([]net.IP, error) {

return addresses, nil
}

func (n *Netbox) queryreverse(host string) ([]string, error) {
var (
ip = dnsutil.ExtractAddressFromReverse(host)
requrl = fmt.Sprintf("%s/?address=%s", n.Url, ip)
records RecordsList
)

// // Initialise an empty slice of domains
domains := make([]string, 0)

// do http request against NetBox instance
resp, err := get(n.Client, requrl, n.Token)
if err != nil {
return domains, fmt.Errorf("Problem performing request: %w", err)
}

// ensure body is closed once we are done
defer resp.Body.Close()

// status code must be http.StatusOK
if resp.StatusCode != http.StatusOK {
return domains, fmt.Errorf("Bad HTTP response code: %d", resp.StatusCode)
}

// read and parse response body
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&records); err != nil {
return domains, fmt.Errorf("Could not unmarshal response: %w", err)
}

// handle empty list of records
if len(records.Records) == 0 {
return domains, nil
}

// grab returned domains
for _, r := range records.Records {
domains = append(domains, strings.TrimSuffix(r.HostName, ".")+".")
}

return domains, nil
}
51 changes: 51 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package netbox

import (
"fmt"
"net"
"testing"

Expand Down Expand Up @@ -125,3 +126,53 @@ func TestQuery(t *testing.T) {
}
}
}

func TestReverseQuery(t *testing.T) {
// set up dummy Netbox
n := newNetbox()
n.Url = "https://example.org/api/ipam/ip-addresses"
n.Token = "mytoken"

tests := []struct {
name string
reverse string
body string
wantErr bool
want []string
}{
{
"Reverse Query",
"2.0.0.10.in-addr.arpa.",
`{
"results": [
{"address": "10.0.0.2", "dns_name": "domain.com"}
]
}`,
false,
[]string{
"domain.com.",
},
},
}

defer gock.Off() // Flush pending mocks after test execution

// set up mock responses
for _, tt := range tests {
gock.New("https://example.org/api/ipam/ip-addresses/").MatchParams(
map[string]string{"address": "10.0.0.2"}).Reply(
200).BodyString(tt.body)
}

// run tests
for _, tt := range tests {
got, err := n.queryreverse(tt.reverse)
if tt.wantErr {
assert.Error(t, err, tt.name)
} else {
fmt.Printf("%#v\n", got)
assert.NoError(t, err, tt.name)
assert.Equal(t, tt.want, got, tt.reverse)
}
}
}

0 comments on commit 289411e

Please sign in to comment.