initial commit

This commit is contained in:
Konstantin Demin 2024-09-14 09:12:10 +03:00
commit 073a3d310a
Signed by: krd
GPG Key ID: 4D56F87A8BA65FD0
18 changed files with 2140 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

55
addr-map-api.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
)
func setupAddrMapApi(r *gin.Engine) {
// TODO
r.GET("/addrmap", func(c *gin.Context) {
var wg sync.WaitGroup
m4 := make([]AddrMap, 0)
m6 := make([]AddrMap, 0)
wg.Add(1)
go func() {
defer wg.Done()
addr4.Range(func(key, value any) bool {
a, ok := value.(AddrMap)
if !ok {
// early return
return true
}
a.Ttl = uint32(a.GetTtl())
m4 = append(m4, a)
return true
})
}()
wg.Add(1)
go func() {
defer wg.Done()
addr6.Range(func(key, value any) bool {
a, ok := value.(AddrMap)
if !ok {
// early return
return true
}
a.Ttl = uint32(a.GetTtl())
m6 = append(m6, a)
return true
})
}()
wg.Wait()
m4 = append(m4, m6...)
c.JSON(http.StatusOK, m4)
})
}

143
addr-map.go Normal file
View File

@ -0,0 +1,143 @@
package main
import (
"crypto/rand"
"encoding/binary"
"log"
"math"
"net"
"sync"
"time"
)
const (
addrMapHouseKeepInterval = time.Second / 4
)
var (
addr4, addr6 sync.Map
)
type AddrMap struct {
SrcAddr net.IP `json:"src"`
DstAddr net.IP `json:"dst"`
Ttl uint32 `json:"ttl"`
Created time.Time `json:"created"`
}
func (a *AddrMap) GetTtl() int32 {
x := math.Trunc(time.Since(a.Created).Round(addrMapHouseKeepInterval).Seconds())
return int32(a.Ttl) - int32(x)
}
func setupAddrMapHousekeeping() {
go func() {
for {
time.Sleep(addrMapHouseKeepInterval)
addr4.Range(func(key, value any) bool {
a, ok := value.(AddrMap)
if ok {
if a.GetTtl() > 1 {
return true
}
}
// delete if value is bogus or if ttl is less than second
addr4.Delete(key)
return true
})
}
}()
go func() {
for {
time.Sleep(addrMapHouseKeepInterval)
addr6.Range(func(key, value any) bool {
a, ok := value.(AddrMap)
if ok {
if a.GetTtl() > 1 {
return true
}
}
// delete if value is bogus or if ttl is less than second
addr6.Delete(key)
return true
})
}
}()
}
func addrMapGet(srcIp net.IP, dstCidr *net.IPNet, ttl uint32) (net.IP, uint32) {
addrlen := len(srcIp)
switch addrlen {
case net.IPv4len, net.IPv6len:
default:
log.Fatalf("addrMapGet(): src size mismatch: %v", addrlen)
}
if addrlen != len(dstCidr.IP) {
log.Fatalf("addrMapGet(): src/dst size mismatch: %v vs %v", addrlen, len(dstCidr.IP))
}
if addrlen != len(dstCidr.Mask) {
log.Fatalf("addrMapGet(): src/dst size mismatch: %v vs %v", addrlen, len(dstCidr.IP))
}
var hkey any
switch addrlen {
case net.IPv4len:
hkey = binary.NativeEndian.Uint32(srcIp)
case net.IPv6len:
hkey = srcIp.To16().String()
}
var curr AddrMap
curr.SrcAddr = make([]byte, addrlen)
curr.DstAddr = make([]byte, addrlen)
copy(curr.DstAddr, srcIp)
curr.Ttl = ttl
_, err := rand.Read(curr.SrcAddr)
if err != nil {
log.Fatalf("rand.Read(): error %v", err)
}
// adjust random bytes to dstCidr
for i := range addrlen / 4 {
a := binary.NativeEndian.Uint32(dstCidr.IP[i*4:])
b := binary.NativeEndian.Uint32(curr.SrcAddr[i*4:])
m := binary.NativeEndian.Uint32(dstCidr.Mask[i*4:])
a += (b & ^m)
binary.NativeEndian.PutUint32(curr.SrcAddr[i*4:], a)
}
curr.Created = time.Now()
var xprev any
var loaded bool
switch addrlen {
case net.IPv4len:
xprev, loaded = addr4.LoadOrStore(hkey, curr)
case net.IPv6len:
xprev, loaded = addr6.LoadOrStore(hkey, curr)
}
if !loaded {
// early return
return curr.SrcAddr, curr.Ttl
}
prev, ok := xprev.(AddrMap)
if !ok {
log.Fatalf("addrMapGet(): wrong value type from sync.Map")
}
copy(curr.SrcAddr, prev.SrcAddr)
if prev.GetTtl() < int32(curr.Ttl) {
switch addrlen {
case net.IPv4len:
addr4.Store(hkey, curr)
case net.IPv6len:
addr6.Store(hkey, curr)
}
} else {
curr.Ttl = uint32(prev.GetTtl())
}
return curr.SrcAddr, curr.Ttl
}

176
cfg.go Normal file
View File

@ -0,0 +1,176 @@
package main
import (
"flag"
"log"
"net"
"strings"
"time"
nft "github.com/google/nftables"
)
var (
cfgListen string
cfgTtlMin uint32
cfgTtlMax uint32
cfgTtlFuzzy bool
cfgResolverEndpoint string
cfgResolverProto string
cfgResolverTimeout time.Duration
cfgWithNft bool
cfgNftTable string
cfgNftTableFamily nft.TableFamily
cfgNftMapV4 string
cfgNftMapV6 string
cfgCidrV4 *net.IPNet
cfgCidrV6 *net.IPNet
cfgSoaNs string
cfgSoaMbox string
)
func init() {
var _cfgTtlMin, _cfgTtlMax uint
var _cfgNftTableFamily, _cfgCidrV4, _cfgCidrV6 string
var _cfgTtlFuzzy bool
flag.StringVar(&cfgListen, "listen", "127.0.0.1:8080", "listen on addr:port")
flag.UintVar(&_cfgTtlMin, "ttl-min", 120, "minimum TTL")
flag.UintVar(&_cfgTtlMax, "ttl-max", 1800, "maximum TTL")
flag.BoolVar(&_cfgTtlFuzzy, "ttl-fuzz", false, "fuzz TTL in DNS responses")
flag.StringVar(&cfgResolverEndpoint, "resolver-endpoint", "127.0.0.1:53", "dns resolver addr:port")
flag.StringVar(&cfgResolverProto, "resolver-proto", "", "dns resolver protocol ('udp' or '' for DNS over UDP, 'tcp' for DNS over TCP, 'tcp-tls' for DNS over TLS)")
flag.DurationVar(&cfgResolverTimeout, "resolver-timeout", 5*time.Second, "dns resolver timeout")
flag.StringVar(&_cfgCidrV4, "cidr-ipv4", "", "IPv4 CIDR mapping (e.g. 192.0.2.0/24)")
flag.StringVar(&_cfgCidrV6, "cidr-ipv6", "", "IPv6 CIDR mapping (e.g. 2001:db8::/64)")
flag.StringVar(&cfgNftTable, "nft-table", "", "nft table name (e.g. 'fw4'); leave empty to not bother with nft")
flag.StringVar(&_cfgNftTableFamily, "nft-table-family", "inet", "nft table family (e.g. 'inet')")
flag.StringVar(&cfgNftMapV4, "nft-map-ipv4", "", "nft IPv4:IPv4 map name")
flag.StringVar(&cfgNftMapV6, "nft-map-ipv6", "", "nft IPv6:IPv6 map name")
flag.StringVar(&cfgSoaNs, "soa-ns", "", "fake SOA name server in dotted form (e.g. 'example.org.')")
flag.StringVar(&cfgSoaMbox, "soa-mbox", "", "fake SOA mailbox in dotted form (e.g. 'dns.example.org.')")
flag.Parse()
if _cfgTtlMin > _cfgTtlMax {
log.Fatalf("invalid ttl range: %d-%d", cfgTtlMin, cfgTtlMax)
}
cfgTtlMin = flagClipTtl(_cfgTtlMin)
cfgTtlMax = flagClipTtl(_cfgTtlMax)
cfgResolverProto = flagResolverProtoMap(cfgResolverProto)
cfgResolverTimeout = flagClipResolverTimeout(cfgResolverTimeout)
cfgWithNft = (cfgNftTable != "")
if cfgWithNft {
cfgNftTableFamily = flagNftTableFamilyMap(_cfgNftTableFamily)
if (cfgNftMapV4 == "") && (cfgNftMapV6 == "") {
log.Fatalf("at least one nft map must be specified")
}
if (cfgNftMapV4 != "") && (_cfgCidrV4 == "") {
log.Fatalf("IPv4: nft map requires CIDR to be specified")
}
if (cfgNftMapV6 != "") && (_cfgCidrV6 == "") {
log.Fatalf("IPv6: nft map requires CIDR to be specified")
}
}
var net_err error
if _cfgCidrV4 != "" {
_, cfgCidrV4, net_err = net.ParseCIDR(_cfgCidrV4)
if net_err != nil {
log.Fatal(net_err)
}
}
if _cfgCidrV6 != "" {
_, cfgCidrV6, net_err = net.ParseCIDR(_cfgCidrV6)
if net_err != nil {
log.Fatal(net_err)
}
}
if (cfgSoaNs == "") || (cfgSoaMbox == "") {
log.Fatalf("both SOA NS and SOA MBOX must be specified")
}
// naive adjustments
if !strings.HasSuffix(cfgSoaNs, ".") {
cfgSoaNs = cfgSoaNs + "."
}
if !strings.HasSuffix(cfgSoaMbox, ".") {
cfgSoaMbox = cfgSoaMbox + "."
}
dnsTtlRange = cfgTtlMax - cfgTtlMin
cfgTtlFuzzy = (_cfgTtlFuzzy && (dnsTtlRange > 10))
}
const (
_ttlMin uint = 30
_ttlMax uint = 86400
_resolverTimeoutMin time.Duration = time.Millisecond
_resolverTimeoutMax time.Duration = 30 * time.Second
)
func flagClipTtl(v uint) uint32 {
if v < _ttlMin {
return uint32(_ttlMin)
}
if v > _ttlMax {
return uint32(_ttlMax)
}
return uint32(v)
}
func flagResolverProtoMap(flag string) string {
switch flag {
case "tcp", "tcp-tls":
return flag
case "udp", "":
return ""
}
log.Fatalf("invalid resolver proto: %s", flag)
// unreachable
return ""
}
func flagClipResolverTimeout(v time.Duration) time.Duration {
if v < _resolverTimeoutMin {
return _resolverTimeoutMin
}
if v > _resolverTimeoutMax {
return _resolverTimeoutMax
}
return v
}
var (
nftTableFamilyFromString = map[string]nft.TableFamily{
"inet": nft.TableFamilyINet,
"ip": nft.TableFamilyIPv4,
"ip6": nft.TableFamilyIPv6,
"arp": nft.TableFamilyARP,
"netdev": nft.TableFamilyNetdev,
"bridge": nft.TableFamilyBridge,
}
)
func flagNftTableFamilyMap(flag string) nft.TableFamily {
if v, ok := nftTableFamilyFromString[flag]; ok {
return v
}
log.Fatalf("invalid nft table family: %s", flag)
// unreachable
return nft.TableFamilyUnspecified
}

358
dns-api.go Normal file
View File

@ -0,0 +1,358 @@
package main
// https://docs.powerdns.com/authoritative/backends/remote.html
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/miekg/dns"
)
const (
dnsApiLight = true
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]
if !dnsApiLight {
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) {
if qtype == dns.TypeSOA {
return []PowerDnsAnswer{
{
Qname: qname,
Qtype: dns.TypeToString[qtype],
Ttl: cfgTtlMax,
Content: fmt.Sprintf("%v %v %v %v %v %v %v",
// ns mbox serial
cfgSoaNs, cfgSoaMbox, time.Now().Unix(),
// refresh retry expire minttl
cfgTtlMax/2, cfgTtlMax, cfgTtlMax*2, cfgTtlMin,
),
},
}, nil
}
resp, err := dnsCustomResolve(qname, qtype)
if err != nil {
return nil, err
}
if resp == nil {
return nil, errors.New("dns.Msg is nil")
}
if len(resp.Answer) == 0 {
log.Printf("got empty answer for %v/%v", qname, dns.TypeToString[qtype])
return []PowerDnsAnswer{}, 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)
}
}

345
dns-remap.go Normal file
View File

@ -0,0 +1,345 @@
package main
import (
"log"
"net"
"sync"
"time"
nft "github.com/google/nftables"
"github.com/miekg/dns"
)
const (
dnsCnameWalkLimit = 16
nftTimeoutSkew = time.Second * 1
)
type DnsAnswer struct {
Qname string
Qtype uint16
Ttl uint32
AddrLen uint32
Addr net.IP
}
func dnsRemap(qname string, qtype uint16, resp *dns.Msg) ([]PowerDnsAnswer, error) {
r_main := make([]dns.RR, 0)
r_extra := make([]dns.RR, 0)
answ := make([]dns.RR, 0, len(resp.Answer)+len(resp.Extra))
answ = append(answ, resp.Answer...)
answ = append(answ, resp.Extra...)
last_qname := qname
needle := qname
name_seen := make(map[string]bool)
name_seen[needle] = true
for range dnsCnameWalkLimit {
i_cnames := make([]int, 0, len(answ))
i_addrs := make([]int, 0, len(answ))
for i := range answ {
switch answ[i].Header().Rrtype {
case dns.TypeCNAME:
i_cnames = append(i_cnames, i)
case dns.TypeA, dns.TypeAAAA:
i_addrs = append(i_addrs, i)
}
}
if len(i_cnames) > 0 {
seen := false
cname_dive := true
for {
if !cname_dive {
break
}
cname_dive = false
for _, i := range i_cnames {
if answ[i].Header().Name != needle {
continue
}
cname := answ[i].(*dns.CNAME)
needle = cname.Target
_, seen = name_seen[needle]
if seen {
// CNAME loop?
log.Printf("CNAME loop: %v -> %v", qname, needle)
return []PowerDnsAnswer{}, nil
}
name_seen[needle] = true
cname_dive = true
break
}
}
}
found := false
for _, i := range i_addrs {
if answ[i].Header().Name != needle {
continue
}
found = true
r_main = append(r_main, answ[i])
}
if found {
for i := range answ {
if !dnsIsAllowedExtraQtype(answ[i].Header().Rrtype) {
continue
}
// if answ[i].Header().Name != needle {
// continue
// }
r_extra = append(r_extra, answ[i])
}
break
}
// trim
answ = answ[:0]
if (needle == last_qname) && (qtype == dns.TypeANY) {
answ = dnsRemapAnyFallback(needle)
continue
}
last_qname = needle
resp, err := dnsCustomResolve(needle, qtype)
if err != nil {
break
}
if resp == nil {
break
}
if len(resp.Answer) != 0 {
for i := range resp.Answer {
answ = append(answ, dns.Copy(resp.Answer[i]))
}
for i := range resp.Extra {
answ = append(answ, dns.Copy(resp.Extra[i]))
}
resp = nil
continue
}
if qtype != dns.TypeANY {
break
}
answ = dnsRemapAnyFallback(needle)
}
if len(r_main) == 0 {
if qname == needle {
log.Printf("not resolved fully %v/%v", qname, dns.TypeToString[qtype])
} else {
log.Printf("not resolved fully %v/%v (stuck at %v)", qname, dns.TypeToString[qtype], needle)
}
return []PowerDnsAnswer{}, nil
}
interim := make([]DnsAnswer, 0, len(r_main))
for i := range r_main {
t := r_main[i].Header().Rrtype
r := DnsAnswer{
Qname: qname,
Qtype: t,
Ttl: r_main[i].Header().Ttl,
}
switch t {
case dns.TypeA:
r.AddrLen = net.IPv4len
r.Addr = make([]byte, net.IPv4len)
copy(r.Addr, r_main[i].(*dns.A).A)
case dns.TypeAAAA:
r.AddrLen = net.IPv6len
r.Addr = make([]byte, net.IPv6len)
copy(r.Addr, r_main[i].(*dns.AAAA).AAAA)
}
interim = append(interim, r)
}
// unify/adjust TTL
var ttl uint32
if cfgTtlFuzzy {
ttl = dnsFuzzClipTtl()
} else {
ttl = cfgTtlMax
for i := range interim {
if ttl > interim[i].Ttl {
ttl = interim[i].Ttl
}
}
ttl = dnsClipTtl(ttl)
}
unix_start := time.Unix(0, 0)
nft_ipv4 := make([]nft.SetElement, 0, len(interim))
nft_ipv6 := make([]nft.SetElement, 0, len(interim))
// remap addresses in answers and prepare nftables maps
for i := range interim {
addrlen := interim[i].AddrLen
var srcAddr net.IP = make([]byte, addrlen)
copy(srcAddr, interim[i].Addr)
var cidr *net.IPNet = nil
switch addrlen {
case net.IPv4len:
if cfgCidrV4 != nil {
cidr = cfgCidrV4
}
case net.IPv6len:
if cfgCidrV6 != nil {
cidr = cfgCidrV6
}
}
if cidr == nil {
// no need to remap or add to nftables
continue
}
dstAddr, nft_ttl := addrMapGet(srcAddr, cidr, ttl)
// HACK: replace addr
copy(interim[i].Addr, dstAddr)
if !cfgWithNft {
continue
}
elem := nft.SetElement{
Key: []byte(dstAddr),
Val: []byte(srcAddr),
// Timeout: time.Duration(nft_ttl),
Timeout: time.Unix(int64(nft_ttl), 0).Add(nftTimeoutSkew).Sub(unix_start).Round(time.Millisecond),
}
switch 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) && (cfgNftMapV4 != "") {
nftDoWithMap(cfgNftTable, cfgNftTableFamily, cfgNftMapV4, func(c *nft.Conn, t *nft.Table, m *nft.Set) error {
_ = c.SetDeleteElements(m, nft_ipv4)
return nil
})
nftDoWithMap(cfgNftTable, cfgNftTableFamily, cfgNftMapV4, func(c *nft.Conn, t *nft.Table, m *nft.Set) error {
return c.SetAddElements(m, nft_ipv4)
})
}
if (len(nft_ipv6) > 0) && (cfgNftMapV6 != "") {
nftDoWithMap(cfgNftTable, cfgNftTableFamily, cfgNftMapV6, func(c *nft.Conn, t *nft.Table, m *nft.Set) error {
_ = c.SetDeleteElements(m, nft_ipv6)
return nil
})
nftDoWithMap(cfgNftTable, cfgNftTableFamily, cfgNftMapV6, func(c *nft.Conn, t *nft.Table, m *nft.Set) error {
return c.SetAddElements(m, nft_ipv6)
})
}
result := make([]PowerDnsAnswer, 0)
for i := range interim {
r := PowerDnsAnswer{
Qname: interim[i].Qname,
Qtype: dns.TypeToString[interim[i].Qtype],
Ttl: ttl,
Content: interim[i].Addr.String(),
}
result = append(result, r)
}
// extra records (if any)
for i := range r_extra {
r, err := dnsRrToPowerDnsAnswer(r_extra[i])
if err != nil {
log.Printf("%v", err)
continue
}
result = append(result, r)
}
// adjust results
for i := range result {
if result[i].Qname == needle {
result[i].Qname = qname
}
}
return result, nil
}
func dnsRemapAnyFallback(qname string) []dns.RR {
var wg sync.WaitGroup
var r_a, r_aaaa []dns.RR
wg.Add(1)
go func() {
defer wg.Done()
resp, err := dnsCustomResolve(qname, dns.TypeA)
if err != nil {
return
}
if resp == nil {
return
}
if len(resp.Answer) == 0 {
return
}
r_a = make([]dns.RR, 0, len(resp.Answer))
for i := range resp.Answer {
r_a = append(r_a, dns.Copy(resp.Answer[i]))
}
for i := range resp.Extra {
r_a = append(r_a, dns.Copy(resp.Extra[i]))
}
}()
wg.Add(1)
go func() {
defer wg.Done()
resp, err := dnsCustomResolve(qname, dns.TypeAAAA)
if err != nil {
return
}
if resp == nil {
return
}
if len(resp.Answer) == 0 {
return
}
r_aaaa = make([]dns.RR, 0, len(resp.Answer))
for i := range resp.Answer {
r_aaaa = append(r_aaaa, dns.Copy(resp.Answer[i]))
}
for i := range resp.Extra {
r_aaaa = append(r_aaaa, dns.Copy(resp.Extra[i]))
}
}()
wg.Wait()
answ := make([]dns.RR, 0, len(r_a)+len(r_aaaa))
// TODO: very naive (no unique record checks)
if r_a != nil {
answ = append(answ, r_a...)
}
if r_aaaa != nil {
answ = append(answ, r_aaaa...)
}
return answ
}

157
dns.go Normal file
View File

@ -0,0 +1,157 @@
package main
import (
"fmt"
"log"
"math/rand"
"net"
"strings"
"sync"
"github.com/miekg/dns"
)
var (
dnsStringToType sync.Map
dnsExtraQtype = make(map[uint16]bool)
dnsTtlRange uint32
)
func setupDns() {
for _, v := range []uint16{
dns.TypePTR, dns.TypeMX, dns.TypeTXT, dns.TypeSRV, dns.TypeLOC, dns.TypeHINFO, dns.TypeSPF,
dns.TypeSIG, dns.TypeKEY, dns.TypeKX, dns.TypeDS, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeNSEC3, dns.TypeNSEC3PARAM, dns.TypeTLSA, dns.TypeCDS, dns.TypeCDNSKEY, dns.TypeZONEMD, dns.TypeTKEY, dns.TypeTSIG, dns.TypeCAA,
dns.TypeSVCB, dns.TypeHTTPS, dns.TypeURI,
} {
dnsExtraQtype[v] = true
}
// warmup
for _, v := range []string{"SOA", "NS", "A", "AAAA", "ANY", "PTR", "CNAME"} {
dnsQtypeStringToValue(v)
}
}
func dnsIsAllowedExtraQtype(qtype uint16) bool {
_, found := dnsExtraQtype[qtype]
return found
}
func dnsQtypeStringToValue(qtype string) uint16 {
x, found := dnsStringToType.Load(qtype)
if found {
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, known := dns.TypeToString[qtype]
if !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
}
func randUint32(v uint32) uint32 {
if v < 2 {
return rand.Uint32()
}
return rand.Uint32() % v
}
func dnsFuzzClipTtl() uint32 {
return dnsClipTtl(cfgTtlMin + 1 + randUint32(dnsTtlRange*2) - dnsTtlRange)
}
type PowerDnsAnswer struct {
Qname string `json:"qname"`
Qtype string `json:"qtype"`
Ttl uint32 `json:"ttl"`
Content string `json:"content"`
}
func dnsRrToPowerDnsAnswer(rr dns.RR) (PowerDnsAnswer, error) {
qname := rr.Header().Name
rrtype := rr.Header().Rrtype
qtype_s, ok := dns.TypeToString[rrtype]
if !ok {
return PowerDnsAnswer{}, fmt.Errorf("record %v: unknown rrtype: %v", qname, rrtype)
}
cont, ok := strings.CutPrefix(rr.String(), rr.Header().String())
if !ok {
return PowerDnsAnswer{}, fmt.Errorf("record %v/%v: unable to produce content", qname, qtype_s)
}
return PowerDnsAnswer{
Qname: qname,
Qtype: qtype_s,
Ttl: dnsClipTtl(rr.Header().Ttl),
Content: strings.TrimSpace(cont),
}, nil
}
func dnsToPowerDnsAnswer(resp *dns.Msg) ([]PowerDnsAnswer, error) {
result := make([]PowerDnsAnswer, 0, len(resp.Answer)+len(resp.Extra))
for i := range resp.Answer {
r, err := dnsRrToPowerDnsAnswer(resp.Answer[i])
if err != nil {
log.Printf("%v", err)
continue
}
result = append(result, r)
}
for i := range resp.Extra {
r, err := dnsRrToPowerDnsAnswer(resp.Extra[i])
if err != nil {
log.Printf("%v", err)
continue
}
result = append(result, r)
}
return result, nil
}

View File

@ -0,0 +1,66 @@
#!/usr/sbin/nft -f
define vnet4 = 198.18.0.0/16
define vnet6 = 2001:db8:1234:5678::/80
table inet uniwall {
map vmap4 { type ipv4_addr : ipv4_addr ; flags dynamic,timeout ; timeout 1m ; }
map vmap6 { type ipv6_addr : ipv6_addr ; flags dynamic,timeout ; timeout 1m ; }
chain rejectx {
reject with icmpx type host-unreachable
drop
}
chain dnat_tele4 {
meta nfproto ipv4 meta l4proto tcp dnat ip to ip daddr map @vmap4
meta nfproto ipv4 meta l4proto udp dnat ip to ip daddr map @vmap4
goto rejectx
}
chain dnat_tele6 {
meta nfproto ipv6 meta l4proto tcp dnat ip6 to ip6 daddr map @vmap6
meta nfproto ipv6 meta l4proto udp dnat ip6 to ip6 daddr map @vmap6
goto rejectx
}
chain dnat_map4 {
ip daddr vmap {
$vnet4 : goto dnat_tele4,
}
return
}
chain dnat_map6 {
ip6 daddr vmap {
$vnet6 : 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,timeout=6000
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 -listen 127.0.0.1:8086 -ttl-fuzz -resolver-endpoint 127.0.0.1:53 -resolver-proto tcp -resolver-timeout 2.9s -cidr-ipv4 198.18.0.0/16 -cidr-ipv6 2001:db8:1234:5678::/80 -nft-table uniwall -nft-map-ipv4 vmap4 -nft-map-ipv6 vmap6 -soa-ns example.org. -soa-mbox dns.example.org.
SyslogIdentifier=powerdns-remote-http-example
Type=exec
Restart=on-failure
RestartSec=3
StartLimitInterval=0
RuntimeDirectory=powerdns-remote-http-example
[Install]
WantedBy=multi-user.target

51
gin.go Normal file
View File

@ -0,0 +1,51 @@
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)
setupAddrMapApi(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)
}

54
go.mod Normal file
View File

@ -0,0 +1,54 @@
module git.krd.sh/krd/powerdns-remote-http-example
go 1.23
require (
github.com/gin-gonic/gin v1.10.0
github.com/google/nftables v0.2.0
github.com/miekg/dns v1.1.62
github.com/prometheus/client_golang v1.20.3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.12.2 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // 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.22.1 // 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/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.59.1 // 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.10.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.25.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

128
go.sum Normal file
View File

@ -0,0 +1,128 @@
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.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg=
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/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.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/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/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
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.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
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/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.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.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8=
golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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=

35
main.go Normal file
View File

@ -0,0 +1,35 @@
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)
setupDns()
setupAddrMapHousekeeping()
setupNftables()
r := setupGin()
log.Printf("%s: ready\n", userAgent)
if err := r.Run(cfgListen); err != nil {
log.Fatal(err)
}
}

197
nft.go Normal file
View File

@ -0,0 +1,197 @@
package main
import (
"fmt"
"log"
"runtime"
nft "github.com/google/nftables"
)
var (
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() {
if !cfgWithNft {
return
}
nftDoWithTable(cfgNftTable, cfgNftTableFamily, func(c *nft.Conn, t *nft.Table) error {
var m4, m6 *nft.Set
var nf_err error
if cfgNftMapV4 != "" {
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)
}
}
if cfgNftMapV6 != "" {
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)
}
}
if m4 != nil {
c.FlushSet(m4)
}
if m6 != nil {
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)
})
}

88
prometheus.go Normal file
View File

@ -0,0 +1,88 @@
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
promOpsProcessed = prometheus.NewCounter(prometheus.CounterOpts{
Name: "processed_ops_total",
Help: "The total number of processed requests",
})
promAddr4Count = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "ipv4_mapped_addr_count",
Help: "The total number of IPv4-mapped addresses",
}, func() float64 {
var x uint32
addr4.Range(func(key, value any) bool {
x++
return true
})
return float64(x)
})
promAddr6Count = prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Name: "ipv6_mapped_addr_count",
Help: "The total number of IPv6-mapped addresses",
}, func() float64 {
var x uint32
addr6.Range(func(key, value any) bool {
x++
return true
})
return float64(x)
})
labelStringReplacer *strings.Replacer = strings.NewReplacer(
"\"", "",
"'", "",
)
)
func setupPrometheus(r *gin.Engine) {
_promRegistry = prometheus.NewRegistry()
_promRegistry.MustRegister(promOpsProcessed)
_promRegistry.MustRegister(promAddr4Count)
_promRegistry.MustRegister(promAddr6Count)
_promHttpHandler = promhttp.HandlerFor(_promRegistry, promhttp.HandlerOpts{
Registry: _promRegistry,
EnableOpenMetrics: true,
ErrorHandling: promhttp.ContinueOnError,
ErrorLog: log.Default(),
DisableCompression: true,
})
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) {
promOpsProcessed.Inc()
_promHttpHandler.ServeHTTP(c.Writer, c.Request)
}