initial commit

This commit is contained in:
Konstantin Demin 2024-06-04 03:20:21 +03:00
commit 856dca3154
Signed by: krd
GPG Key ID: 4D56F87A8BA65FD0
17 changed files with 1605 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/powerdns-remote-http-example

175
LICENSE Normal file
View File

@ -0,0 +1,175 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

70
Makefile Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/make -f
# SPDX-License-Identifier: Apache-2.0
# (c) 2024, Konstantin Demin
SHELL :=/bin/sh
.SHELLFLAGS :=-ec
.NOTPARALLEL:
BIN := powerdns-remote-http-example
OUTDIR ?= .
OUTSFX ?=
OUTBIN ?= $(OUTDIR)/$(BIN)$(OUTSFX)
export GO ?= go
export CGO_ENABLED ?= 0
## "purego" - https://github.com/cespare/xxhash
TAGS ?= purego
LDFLAGS ?=
GO_BUILDFLAGS ?=
GO_LDFLAGS := -w $(LDFLAGS)
comma :=,
ifeq ($(RELMODE),1)
## not ready yet
# TAGS := nodebug$(if $(strip $(TAGS)),$(comma)$(strip $(TAGS)))
GO_LDFLAGS += -s
endif
.PHONY: all
all: build
.PHONY: clean build dev-build ci-clean
clean:
$(if $(wildcard $(OUTBIN)),rm -fv $(OUTBIN),:)
build: $(OUTBIN)
test_git = git -c log.showsignature=false show -s --format=%H:%ct
$(OUTBIN):
@:; \
GO_BUILDFLAGS='$(strip $(GO_BUILDFLAGS))' ; \
if ! $(test_git) >/dev/null 2>&1 ; then \
echo "!!! git information is asbent !!!" >&2 ; \
GO_BUILDFLAGS="-buildvcs=false $${GO_BUILDFLAGS}" ; \
fi ; \
for i in $$(seq 1 3) ; do \
if $(GO) get ; then break ; fi ; \
done ; \
$(GO) build -o $@ \
$${GO_BUILDFLAGS} \
$(if $(strip $(TAGS)),-tags '$(strip $(TAGS))') \
$(if $(strip $(GO_LDFLAGS)),-ldflags '$(strip $(GO_LDFLAGS))') \
$(if $(VERBOSE),-v) ; \
$(GO) version -m $@
dev-build: GO_BUILDFLAGS := -race $(GO_BUILDFLAGS)
dev-build: CGO_ENABLED := 1
dev-build: RELMODE := 0
dev-build: build
ci-clean:
for d in '$(shell $(GO) env GOCACHE)' '$(shell $(GO) env GOMODCACHE)' ; do \
[ -n "$$d" ] || continue ; \
[ -d "$$d" ] || continue ; \
rm -rf "$$d" ; \
done

43
addr-map.go Normal file
View File

@ -0,0 +1,43 @@
package main
import (
"encoding/binary"
"log"
"net"
"github.com/cespare/xxhash/v2"
)
// TODO: write more convenient version
func addrMap(srcIp net.IP, dstCidr *net.IPNet) net.IP {
addrlen := len(srcIp)
if addrlen != len(dstCidr.IP) {
log.Fatalf("addrMap(): src/dst size mismatch: %v vs %v", len(srcIp), len(dstCidr.IP))
}
var addr net.IP = make([]byte, addrlen)
hsum := xxhash.Sum64(srcIp)
bsum := binary.NativeEndian.AppendUint64([]byte{}, hsum)
blen := len(bsum)
if addrlen > blen {
// extend bsum
tmp := append(make([]byte, addrlen-blen), bsum...)
bsum = tmp
}
if addrlen < blen {
// trim bsum
bsum = bsum[:addrlen]
}
// apply inverted mask to bsum and sum with addr
for i := range addrlen / 4 {
a := binary.NativeEndian.Uint32(dstCidr.IP[i*4:])
b := binary.NativeEndian.Uint32(bsum[i*4:])
m := binary.NativeEndian.Uint32(dstCidr.Mask[i*4:])
a += (b & ^m)
binary.NativeEndian.PutUint32(addr[i*4:], a)
}
return addr
}

25
cfg.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"time"
nft "github.com/google/nftables"
)
const (
cfgListen = "127.0.0.1:8086"
cfgTtlMin uint32 = 60
cfgTtlMax uint32 = 3600
cfgResolverEndpoint = "127.1.0.1:53"
cfgResolverProto = "tcp"
cfgResolverTimeout = 1500 * time.Millisecond
cfgNftTable = "uni"
cfgNftTableFamily = nft.TableFamilyINet
cfgNftMapV4 = "tele4"
cfgNftMapV6 = "tele6"
cfgNftCidrV4 = "251.0.0.0/8"
cfgNftCidrV6 = "2001:db8:11::/48"
)

331
dns-api.go Normal file
View File

@ -0,0 +1,331 @@
package main
// https://docs.powerdns.com/authoritative/backends/remote.html
import (
"encoding/json"
"errors"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/miekg/dns"
)
const (
dnsApiRootUri = "/dns"
dnsApiUriGet = dnsApiRootUri + "/:method/*parameters"
dnsApiUriPostForm = dnsApiRootUri + "/:method"
dnsApiUriPostJson = dnsApiRootUri
)
var (
dnsApiMethods = map[string]dnsApiHandlerFunc{
"lookup": dnsApi_lookup,
"getAllDomains": dnsApi_emptyArray,
"getDomainMetadata": dnsApi_emptyArray,
"getAllDomainMetadata": dnsApi_emptyMap,
}
)
func setupDnsApi(r *gin.Engine) {
r.GET(dnsApiUriGet, dnsApiHttpGetHandler)
// https://docs.powerdns.com/authoritative/backends/remote.html#http-connector
// "post" in "remote-connection-string="
r.POST(dnsApiUriPostForm, dnsApiHttpPostFormHandler)
// https://docs.powerdns.com/authoritative/backends/remote.html#http-connector
// "post,post_json" in "remote-connection-string="
r.POST(dnsApiUriPostJson, dnsApiHttpPostJsonHandler)
}
func dnsApiNotImplemented(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"result": false})
}
func dnsApiSimpleError(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"result": false})
}
func dnsApiGenericError(c *gin.Context, err error) {
log.Printf("error: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
func dnsApiMissingParameters(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": "missing parameters"})
}
func dnsApiArgumentError(c *gin.Context, argument string, err error) {
log.Printf("argument %v error: %v", argument, err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
func dnsApiMethodError(c *gin.Context, method string, err error) {
log.Printf("method %v returned error: %v", method, err)
c.JSON(http.StatusBadRequest, gin.H{"result": false, "error": err.Error()})
}
type PowerDnsJsonRequest struct {
Method string `json:"method"`
Parameters map[string]interface{} `json:"parameters"`
}
func NewPowerDnsRequest() PowerDnsJsonRequest {
var r PowerDnsJsonRequest
r.Parameters = make(map[string]interface{})
return r
}
type dnsApiHandlerFunc func(*PowerDnsJsonRequest) (interface{}, error)
func dnsApiHttpGetHandler(c *gin.Context) {
var err error
method := c.Param("method")
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
// HTTP GET scheme is somewhat spoiled
uriParams := make([]string, 0)
uriParamPrefix := dnsApiRootUri + "/" + method + "/"
uriParamString, hasUriParams := strings.CutPrefix(c.Request.RequestURI, uriParamPrefix)
if hasUriParams {
if uriParamString == "" {
dnsApiMissingParameters(c)
return
}
uriParams = strings.Split(uriParamString, "/")
} else {
uriParamString = ""
}
req := NewPowerDnsRequest()
req.Method = method
// HTTP GET scheme is somewhat spoiled, part 2
switch method {
case "lookup":
switch len(uriParams) {
case 0:
dnsApiArgumentError(c, "qname", err)
return
case 1:
dnsApiArgumentError(c, "qtype", err)
return
}
req.Parameters["qname"] = uriParams[0]
req.Parameters["qtype"] = uriParams[1]
for _, p := range []string{"remote", "local", "real-remote"} {
val := c.Request.Header.Get("X-RemoteBackend-" + p)
if val == "" {
continue
}
req.Parameters[p] = val
}
for _, p := range []string{"zone-id", "zone_id"} {
val := c.Request.Header.Get("X-RemoteBackend-" + p)
if val == "" {
continue
}
req.Parameters["zone-id"], err = strconv.ParseInt(val, 10, 64)
if err != nil {
dnsApiArgumentError(c, "zone-id", err)
return
}
}
case "getAllDomains":
req.Parameters["include_disabled"], err = strconv.ParseBool(c.DefaultQuery("include_disabled", "false"))
if err != nil {
dnsApiArgumentError(c, "include_disabled", err)
return
}
case "getAllDomainMetadata":
if len(uriParams) == 0 {
dnsApiArgumentError(c, "name", err)
return
}
req.Parameters["name"] = uriParams[0]
case "getDomainMetadata":
switch len(uriParams) {
case 0:
dnsApiArgumentError(c, "name", err)
return
case 1:
dnsApiArgumentError(c, "kind", err)
return
}
req.Parameters["name"] = uriParams[0]
req.Parameters["kind"] = uriParams[1]
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApiHttpPostFormHandler(c *gin.Context) {
var err error
method := c.Param("method")
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
params := c.PostForm("parameters")
if params == "" {
dnsApiMissingParameters(c)
return
}
if strings.HasPrefix(params, "%7b") {
params, err = url.PathUnescape(params)
if err != nil {
dnsApiGenericError(c, err)
return
}
}
req := NewPowerDnsRequest()
req.Method = method
if strings.HasPrefix(params, "{") {
err = json.Unmarshal([]byte(params), &req.Parameters)
if err != nil {
dnsApiGenericError(c, err)
return
}
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApiHttpPostJsonHandler(c *gin.Context) {
var err error
bodyData, err := io.ReadAll(c.Request.Body)
if err != nil {
dnsApiGenericError(c, err)
return
}
req := NewPowerDnsRequest()
err = json.Unmarshal(bodyData, &req)
if err != nil {
dnsApiGenericError(c, err)
return
}
method := req.Method
if method == "" {
dnsApiSimpleError(c)
return
}
mFunc, mFound := dnsApiMethods[method]
if !mFound {
dnsApiNotImplemented(c)
return
}
result, err := mFunc(&req)
if err != nil {
dnsApiMethodError(c, method, err)
return
}
c.JSON(http.StatusOK, gin.H{"result": result})
}
func dnsApi_emptyArray(r *PowerDnsJsonRequest) (interface{}, error) {
emptyArray := make([]int, 0)
return emptyArray, nil
}
func dnsApi_emptyMap(r *PowerDnsJsonRequest) (interface{}, error) {
emptyMap := make(map[string]interface{}, 0)
return emptyMap, nil
}
func dnsApi_lookup(r *PowerDnsJsonRequest) (interface{}, error) {
x, ok := r.Parameters["qtype"]
if !ok {
return nil, errors.New("qtype is absent")
}
qtype, ok := x.(string)
if !ok {
return nil, errors.New("qtype is not a string")
}
if qtype == "" {
return nil, errors.New("qtype is empty")
}
dns_qtype := dnsQtypeStringToValue(qtype)
if dns_qtype == dns.TypeNone {
return nil, errors.New("qtype is NONE")
}
x, ok = r.Parameters["qname"]
if !ok {
return nil, errors.New("qname is absent")
}
qname, ok := x.(string)
if !ok {
return nil, errors.New("qname is not a string")
}
if qname == "" {
return nil, errors.New("qname is empty")
}
// adjust qname
if !strings.HasSuffix(qname, ".") {
qname = qname + "."
}
return dnsApi_lookup_int(qname, dns_qtype)
}
func dnsApi_lookup_int(qname string, qtype uint16) (interface{}, error) {
resp, err := dnsCustomResolve(qname, qtype)
if err != nil {
return nil, err
}
if resp == nil {
return nil, errors.New("dns.Msg is nil")
}
switch qtype {
case dns.TypeA, dns.TypeAAAA, dns.TypeANY:
// resolve and process (see below)
return dnsRemap(qname, qtype, resp)
default:
// only resolve
return dnsToPowerDnsAnswer(resp)
}
}

229
dns-remap.go Normal file
View File

@ -0,0 +1,229 @@
package main
import (
"net"
"sync"
"time"
nft "github.com/google/nftables"
"github.com/miekg/dns"
)
type DnsAnswer struct {
Qname string
Qtype uint16
Ttl uint32
AddrLen uint32
Addr net.IP
}
func dnsRemap(qname string, qtype uint16, orig *dns.Msg) ([]PowerDnsAnswer, error) {
interim := make([]DnsAnswer, 0, len(orig.Answer))
real_qnames := make([]string, 0)
for _, rr := range orig.Answer {
if rr.Header().Name != qname {
continue
}
r := DnsAnswer{
Qname: qname,
Qtype: rr.Header().Rrtype,
Ttl: rr.Header().Ttl,
}
switch r.Qtype {
case dns.TypeA:
r.Addr = rr.(*dns.A).A
r.AddrLen = net.IPv4len
interim = append(interim, r)
case dns.TypeAAAA:
r.Addr = rr.(*dns.AAAA).AAAA
r.AddrLen = net.IPv6len
interim = append(interim, r)
case dns.TypeCNAME:
cname := rr.(*dns.CNAME)
real_qnames = append(real_qnames, cname.Target)
}
}
var wg sync.WaitGroup
var mtx_interim sync.Mutex
// reprocess answers due to CNAME
for _, real_qname := range real_qnames {
wg.Add(1)
go func(real_name string) {
defer wg.Done()
found_qname := false
for _, rr := range orig.Answer {
if rr.Header().Name != real_name {
continue
}
found_qname = true
r := DnsAnswer{
Qname: qname,
Qtype: rr.Header().Rrtype,
Ttl: rr.Header().Ttl,
}
switch r.Qtype {
case dns.TypeA:
r.Addr = rr.(*dns.A).A
r.AddrLen = net.IPv4len
mtx_interim.Lock()
interim = append(interim, r)
mtx_interim.Unlock()
case dns.TypeAAAA:
r.Addr = rr.(*dns.AAAA).AAAA
r.AddrLen = net.IPv6len
mtx_interim.Lock()
interim = append(interim, r)
mtx_interim.Unlock()
}
}
if found_qname {
return
}
resp, err := dnsCustomResolve(real_name, dns.TypeANY)
if err != nil {
return
}
if resp == nil {
return
}
for _, rr := range resp.Answer {
if rr.Header().Name != real_name {
continue
}
r := DnsAnswer{
Qname: qname,
Qtype: rr.Header().Rrtype,
Ttl: rr.Header().Ttl,
}
switch r.Qtype {
case dns.TypeA:
r.Addr = rr.(*dns.A).A
r.AddrLen = net.IPv4len
mtx_interim.Lock()
interim = append(interim, r)
mtx_interim.Unlock()
case dns.TypeAAAA:
r.Addr = rr.(*dns.AAAA).AAAA
r.AddrLen = net.IPv6len
mtx_interim.Lock()
interim = append(interim, r)
mtx_interim.Unlock()
}
}
}(real_qname)
}
wg.Wait()
result := make([]PowerDnsAnswer, 0, len(interim))
// nothing to do
if len(interim) == 0 {
return result, nil
}
unix_start := time.Unix(0, 0)
nft_ipv4 := make([]nft.SetElement, 0, len(interim))
nft_ipv6 := make([]nft.SetElement, 0, len(interim))
// fill map elements
for _, r := range interim {
switch r.AddrLen {
case net.IPv4len, net.IPv6len:
break
default:
continue
}
var srcAddr net.IP = make([]byte, r.AddrLen)
copy(srcAddr, r.Addr)
var dstAddr net.IP
switch r.AddrLen {
case net.IPv4len:
dstAddr = addrMap(r.Addr, nftCidrV4)
case net.IPv6len:
dstAddr = addrMap(r.Addr, nftCidrV6)
}
// HACK: clip ttl
r.Ttl = dnsClipTtl(r.Ttl)
// HACK: replace addr
copy(r.Addr, dstAddr)
elem := nft.SetElement{
Key: []byte(dstAddr),
Val: []byte(srcAddr),
// Timeout: time.Duration(r.Ttl),
Timeout: time.Unix(int64(r.Ttl), 0).Sub(unix_start),
}
switch r.AddrLen {
case net.IPv4len:
nft_ipv4 = append(nft_ipv4, elem)
case net.IPv6len:
nft_ipv6 = append(nft_ipv6, elem)
}
}
// perform nftables assignment
if len(nft_ipv4) > 0 {
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
m, err := nftGetMapByName(c, t, cfgNftMapV4)
if err != nil {
return err
}
_ = c.SetDeleteElements(m, nft_ipv4)
return nil
})
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
m, err := nftGetMapByName(c, t, cfgNftMapV4)
if err != nil {
return err
}
return c.SetAddElements(m, nft_ipv4)
})
}
if len(nft_ipv6) > 0 {
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
m, err := nftGetMapByName(c, t, cfgNftMapV6)
if err != nil {
return err
}
_ = c.SetDeleteElements(m, nft_ipv6)
return nil
})
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
m, err := nftGetMapByName(c, t, cfgNftMapV6)
if err != nil {
return err
}
return c.SetAddElements(m, nft_ipv6)
})
}
for _, i := range interim {
t, ok := dns.TypeToString[i.Qtype]
if !ok {
continue
}
r := PowerDnsAnswer{
Qname: i.Qname,
Qtype: t,
Ttl: i.Ttl,
Content: i.Addr.String(),
}
result = append(result, r)
}
return result, nil
}

108
dns.go Normal file
View File

@ -0,0 +1,108 @@
package main
import (
"errors"
"fmt"
"log"
"net"
"strings"
"sync"
"github.com/miekg/dns"
)
var (
dnsStringToType sync.Map
)
func dnsQtypeStringToValue(qtype string) uint16 {
x, found_qtype := dnsStringToType.Load(qtype)
if found_qtype {
return x.(uint16)
}
for k, v := range dns.TypeToString {
if v == qtype {
dnsStringToType.Store(qtype, k)
return k
}
}
log.Printf("qtype %#v is not known or found", qtype)
return dns.TypeNone
}
func dnsCustomResolve(qname string, qtype uint16) (*dns.Msg, error) {
qtype_s, qtype_known := dns.TypeToString[qtype]
if !qtype_known {
log.Printf("qtype is not known (%v) for %v", qtype, qname)
return nil, fmt.Errorf("qtype is not known: %v", qtype)
}
c := new(dns.Client)
c.Net = cfgResolverProto
c.Dialer = &net.Dialer{
Timeout: cfgResolverTimeout,
}
req := new(dns.Msg)
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = make([]dns.Question, 1)
req.Question[0] = dns.Question{Name: qname, Qtype: qtype, Qclass: dns.ClassINET}
resp, rtt, err := c.Exchange(req, cfgResolverEndpoint)
if err != nil {
log.Printf("resolving %v/%v (rtt %v) with error: %v", qname, qtype_s, rtt, err)
return nil, err
}
log.Printf("resolved %v/%v (rtt %v)", qname, qtype_s, rtt)
return resp, nil
}
func dnsClipTtl(ttl uint32) uint32 {
if ttl < cfgTtlMin {
return cfgTtlMin
}
if ttl > cfgTtlMax {
return cfgTtlMax
}
return ttl
}
type PowerDnsAnswer struct {
Qname string `json:"qname"`
Qtype string `json:"qtype"`
Ttl uint32 `json:"ttl"`
Content string `json:"content"`
}
func dnsToPowerDnsAnswer(response *dns.Msg) ([]PowerDnsAnswer, error) {
if response == nil {
return nil, errors.New("response is nil")
}
result := make([]PowerDnsAnswer, 0, len(response.Answer))
for i := range response.Answer {
cont, ok := strings.CutPrefix(response.Answer[i].String(), response.Answer[i].Header().String())
if !ok {
continue
}
qtype, ok := dns.TypeToString[response.Answer[i].Header().Rrtype]
if !ok {
continue
}
rec := PowerDnsAnswer{
Qname: response.Answer[i].Header().Name,
Qtype: qtype,
Ttl: dnsClipTtl(response.Answer[i].Header().Ttl),
Content: cont,
}
result = append(result, rec)
}
return result, nil
}

View File

@ -0,0 +1,66 @@
#!/usr/sbin/nft -f
define n_tele4 = 251.0.0.0/8
define n_tele6 = 2001:db8:11::/48
table inet uni {
map tele4 { type ipv4_addr : ipv4_addr ; flags dynamic,timeout ; timeout 1m ; }
map tele6 { type ipv6_addr : ipv6_addr ; flags dynamic,timeout ; timeout 1m ; }
chain gtfo {
reject with icmpx type host-unreachable
drop
}
chain dnat_tele4 {
meta nfproto ipv4 meta l4proto tcp dnat ip to ip daddr map @tele4
meta nfproto ipv4 meta l4proto udp dnat ip to ip daddr map @tele4
goto gtfo
}
chain dnat_tele6 {
meta nfproto ipv6 meta l4proto tcp dnat ip6 to ip6 daddr map @tele6
meta nfproto ipv6 meta l4proto udp dnat ip6 to ip6 daddr map @tele6
goto gtfo
}
chain dnat_map4 {
ip daddr vmap {
$n_tele4 : goto dnat_tele4,
}
return
}
chain dnat_map6 {
ip6 daddr vmap {
$n_tele6 : goto dnat_tele6,
}
return
}
chain nat_prerouting {
type nat hook prerouting priority dstnat;
meta nfproto vmap {
ipv4 : jump dnat_map4,
ipv6 : jump dnat_map6,
}
}
chain nat_output {
type nat hook output priority dstnat;
meta nfproto vmap {
ipv4 : jump dnat_map4,
ipv6 : jump dnat_map6,
}
}
chain nat_postrouting {
type nat hook postrouting priority srcnat;
meta oiftype != loopback masquerade
}
}

25
example-conf/pdns.conf Normal file
View File

@ -0,0 +1,25 @@
launch=remote
remote-connection-string=http:url=http://127.0.0.1:8086/dns,post,post_json
consistent-backends=false
zone-cache-refresh-interval=0
non-local-bind=yes
local-address-nonexist-fail=no
local-address=127.0.0.1
local-port=5371
no-shuffle=on
default-ttl=60
cache-ttl=0
query-cache-ttl=0
negquery-cache-ttl=30
max-cache-entries=0
max-packet-cache-entries=0
receiver-threads=2
reuseport=yes
tcp-fast-open=20
disable-axfr=yes
security-poll-suffix=

View File

@ -0,0 +1,16 @@
[Unit]
Description=PowerDNS Remote/HTTP backend example
Wants=network-online.target
After=network-online.target time-sync.target
[Service]
ExecStart=/usr/local/sbin/powerdns-remote-http-example
SyslogIdentifier=powerdns-remote-http-example
Type=exec
Restart=on-failure
RestartSec=2
StartLimitInterval=0
RuntimeDirectory=powerdns-remote-http-example
[Install]
WantedBy=multi-user.target

50
gin.go Normal file
View File

@ -0,0 +1,50 @@
package main
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func setupGin() *gin.Engine {
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode)
}
r := ginNewEngine()
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "UP"})
})
setupDnsApi(r)
setupPrometheus(r)
return r
}
func ginNewEngine() *gin.Engine {
var r *gin.Engine
switch gin.Mode() {
case gin.ReleaseMode:
r = gin.New()
ginCustomLogger(r)
default:
r = gin.Default()
}
r.Use(ginSetServerHeader)
return r
}
func ginCustomLogger(r *gin.Engine) {
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{
"/health",
"/metrics/",
},
}))
}
func ginSetServerHeader(c *gin.Context) {
c.Writer.Header().Set("Server", userAgent)
}

52
go.mod Normal file
View File

@ -0,0 +1,52 @@
module git.krd.sh/krd/powerdns-remote-http-example
go 1.22
require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/gin-gonic/gin v1.10.0
github.com/google/nftables v0.2.0
github.com/miekg/dns v1.1.59
github.com/prometheus/client_golang v1.19.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.8 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.21.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

125
go.sum Normal file
View File

@ -0,0 +1,125 @@
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/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LRA=
github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0=
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8=
github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
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.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4=
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

33
main.go Normal file
View File

@ -0,0 +1,33 @@
package main
import (
"log"
"runtime"
)
const (
appName = "powerdns-remote-http-example"
appVersion = "0.0.1"
userAgent = appName + "/" + appVersion
minimumGoMaxProcs = 4
)
func main() {
gmp := runtime.GOMAXPROCS(0)
if gmp < minimumGoMaxProcs {
runtime.GOMAXPROCS(minimumGoMaxProcs)
}
log.SetFlags(log.Flags() | log.Lmicroseconds)
log.Printf("%s: starting\n", userAgent)
setupNftables()
r := setupGin()
log.Printf("%s: ready\n", userAgent)
if err := r.Run(cfgListen); err != nil {
log.Fatal(err)
}
}

195
nft.go Normal file
View File

@ -0,0 +1,195 @@
package main
import (
"fmt"
"log"
"net"
"runtime"
nft "github.com/google/nftables"
)
var (
nftCidrV4 *net.IPNet
nftCidrV6 *net.IPNet
nftTableFamilyToString = map[nft.TableFamily]string{
nft.TableFamilyINet: "inet",
nft.TableFamilyIPv4: "ip",
nft.TableFamilyIPv6: "ip6",
nft.TableFamilyARP: "arp",
nft.TableFamilyNetdev: "netdev",
nft.TableFamilyBridge: "bridge",
}
)
func setupNftables() {
var net_err error
_, nftCidrV4, net_err = net.ParseCIDR(cfgNftCidrV4)
if net_err != nil {
log.Fatal(net_err)
}
_, nftCidrV6, net_err = net.ParseCIDR(cfgNftCidrV6)
if net_err != nil {
log.Fatal(net_err)
}
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
m4, nf_err := nftGetMapByName(c, t, cfgNftMapV4)
if nf_err != nil {
log.Fatal(nf_err)
}
if m4 == nil {
log.Fatalf("unable to find nft map %#v", cfgNftMapV4)
}
m6, nf_err := nftGetMapByName(c, t, cfgNftMapV6)
if nf_err != nil {
log.Fatal(nf_err)
}
if m6 == nil {
log.Fatalf("unable to find nft map %#v", cfgNftMapV6)
}
c.FlushSet(m4)
c.FlushSet(m6)
return nil
})
}
func nftDo(actor func(*nft.Conn) error) error {
if actor == nil {
log.Fatal("nftDo(): actor is nil")
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
c, err := nft.New()
if err != nil {
log.Printf("nft.New() error: %v", err)
log.Panic(err)
}
if c == nil {
log.Fatal("nft.New() returned nil")
}
err = actor(c)
if err == nil {
err = c.Flush()
}
return err
}
func nftGetTableByName(nftConn *nft.Conn, tableName string, tableFamily nft.TableFamily) (*nft.Table, error) {
var err error
var all []*nft.Table
if tableFamily == nft.TableFamilyUnspecified {
all, err = nftConn.ListTables()
if err != nil {
log.Printf("nft.ListTables: %v", err)
return nil, err
}
if all == nil {
log.Fatal("nft.Conn.ListTables() returned nil")
}
} else {
all, err = nftConn.ListTablesOfFamily(tableFamily)
if err != nil {
log.Printf("nft.ListTablesOfFamily: %v", err)
return nil, err
}
if all == nil {
log.Fatal("nft.Conn.ListTablesOfFamily() returned nil")
}
}
var table *nft.Table
for i := range all {
if all[i].Name != tableName {
continue
}
table = new(nft.Table)
*table = *(all[i])
break
}
return table, nil
}
func nftDoWithTable(tableName string, tableFamily nft.TableFamily, actor func(*nft.Conn, *nft.Table) error) error {
if actor == nil {
log.Fatal("nftDoWithTable(): actor is nil")
}
return nftDo(func(c *nft.Conn) error {
t, err := nftGetTableByName(c, tableName, tableFamily)
if err != nil {
return err
}
if t == nil {
if tableFamily == nft.TableFamilyUnspecified {
return fmt.Errorf("unable to find nft table %#v", tableName)
}
family, ok := nftTableFamilyToString[tableFamily]
if ok {
return fmt.Errorf("unable to find nft table %#v (family %v)", tableName, family)
} else {
return fmt.Errorf("unable to find nft table %#v (family id = %v)", tableName, tableFamily)
}
}
return actor(c, t)
})
}
func nftDoWithSet(tableName string, tableFamily nft.TableFamily, setName string, actor func(*nft.Conn, *nft.Table, *nft.Set) error) error {
if actor == nil {
log.Fatal("nftDoWithSet(): actor is nil")
}
return nftDoWithTable(tableName, tableFamily, func(c *nft.Conn, t *nft.Table) error {
s, err := c.GetSetByName(t, setName)
if err != nil {
return err
}
if s == nil {
return fmt.Errorf("unable to find nft set %#v", setName)
}
return actor(c, t, s)
})
}
func nftGetMapByName(nftConn *nft.Conn, nftTable *nft.Table, mapName string) (*nft.Set, error) {
m, err := nftConn.GetSetByName(nftTable, mapName)
if err != nil {
return nil, err
}
if m == nil {
return nil, nil
}
if !m.IsMap {
return nil, fmt.Errorf("nft set %#v is not map", mapName)
}
return m, nil
}
func nftDoWithMap(tableName string, tableFamily nft.TableFamily, mapName string, actor func(*nft.Conn, *nft.Table, *nft.Set) error) error {
if actor == nil {
log.Fatal("nftDoWithMap(): actor is nil")
}
return nftDoWithTable(tableName, tableFamily, func(c *nft.Conn, t *nft.Table) error {
m, err := nftGetMapByName(c, t, mapName)
if err != nil {
return err
}
if m == nil {
return fmt.Errorf("unable to find nft map %#v", mapName)
}
return actor(c, t, m)
})
}

61
prometheus.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// TODO: more metrics
var (
_promRegistry *prometheus.Registry
_promHttpHandler http.Handler
opsProcessed = prometheus.NewCounter(prometheus.CounterOpts{
Name: "processed_ops_total",
Help: "The total number of processed requests",
})
labelStringReplacer *strings.Replacer = strings.NewReplacer(
"\"", "",
"'", "",
)
)
func setupPrometheus(r *gin.Engine) {
_promRegistry = prometheus.NewRegistry()
_promRegistry.MustRegister(opsProcessed)
_promHttpHandler = promhttp.HandlerFor(_promRegistry, promhttp.HandlerOpts{
Registry: _promRegistry,
EnableOpenMetrics: true,
ErrorHandling: promhttp.ContinueOnError,
ErrorLog: log.Default(),
})
r.GET("/metrics", promHttpHandler)
r.GET("/metrics/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/metrics")
})
}
func promSanitizeLabel(str string, fallback string) string {
out := strings.ToValidUTF8(str, "")
out = labelStringReplacer.Replace(out)
out = strings.TrimSpace(out)
if out == "" {
return fallback
}
return out
}
func promHttpHandler(c *gin.Context) {
opsProcessed.Inc()
_promHttpHandler.ServeHTTP(c.Writer, c.Request)
}