summaryrefslogtreecommitdiff
path: root/provider/bunny.go
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-08-20 21:57:55 +0200
committerMel <mel@rnrd.eu>2026-08-20 21:57:55 +0200
commita81cf3b385e45587a6be51f7958f9042975fc0c6 (patch)
tree1588267cfde82d3e831db2c4e9d411dfb55f5489 /provider/bunny.go
parent6a4c0a16118d0b545c3c5f6a08ecad432940c1cf (diff)
downloadddnnss-a81cf3b385e45587a6be51f7958f9042975fc0c6.tar.zst
ddnnss-a81cf3b385e45587a6be51f7958f9042975fc0c6.zip
Add bunny.net provider
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'provider/bunny.go')
-rw-r--r--provider/bunny.go117
1 files changed, 117 insertions, 0 deletions
diff --git a/provider/bunny.go b/provider/bunny.go
new file mode 100644
index 0000000..c550a78
--- /dev/null
+++ b/provider/bunny.go
@@ -0,0 +1,117 @@
+package provider
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/netip"
+ "net/url"
+)
+
+type bunny struct {
+ log *slog.Logger
+ key string
+}
+
+var _ Provider = (*bunny)(nil)
+
+func newBunny(log *slog.Logger, cfg Config) (*bunny, error) {
+ key, err := cfg.key()
+ if err != nil {
+ return nil, fmt.Errorf("failed instantiating bunny provider: %w", err)
+ }
+ return &bunny{key: key, log: log}, nil
+}
+
+const bunnyAPI = "https://api.bunny.net/dnszone"
+
+// bunny encodes record types numerically.
+var bunnyTypes = map[RecordType]int{TypeA: 0, TypeAAAA: 1}
+
+func (b *bunny) headers() map[string]string {
+ return map[string]string{"AccessKey": b.key}
+}
+
+func (b *bunny) Ensure(zone, name string, rtype RecordType, addr netip.Addr) (string, bool, error) {
+ b.log.Info("updating record via bunny.net api")
+
+ var zones struct {
+ Items []struct {
+ ID int64 `json:"Id"`
+ Domain string `json:"Domain"`
+ } `json:"Items"`
+ }
+ raw, err := request("GET", bunnyAPI+"?search="+url.QueryEscape(zone),
+ b.headers(), nil)
+ if err != nil {
+ return "", false, err
+ }
+ if err := json.Unmarshal(raw, &zones); err != nil {
+ return "", false, err
+ }
+ var zoneID int64 = -1
+ for _, z := range zones.Items {
+ if z.Domain == zone {
+ zoneID = z.ID
+ }
+ }
+ if zoneID < 0 {
+ return "", false, fmt.Errorf("zone %s not found in bunny", zone)
+ }
+
+ var details struct {
+ Records []struct {
+ ID int64 `json:"Id"`
+ Type int `json:"Type"`
+ Name string `json:"Name"`
+ Value string `json:"Value"`
+ } `json:"Records"`
+ }
+ raw, err = request("GET", fmt.Sprintf("%s/%d", bunnyAPI, zoneID), b.headers(), nil)
+ if err != nil {
+ return "", false, fmt.Errorf("failed to request zone details from bunny: %w", err)
+ }
+ if err := json.Unmarshal(raw, &details); err != nil {
+ return "", false, fmt.Errorf("received malformed zone details from bunny: %w", err)
+ }
+
+ body, _ := json.Marshal(map[string]any{
+ "Type": bunnyTypes[rtype],
+ "Name": name,
+ "Value": addr.String(),
+ "Ttl": 300,
+ })
+
+ for _, r := range details.Records {
+ if r.Type != bunnyTypes[rtype] || r.Name != name {
+ continue
+ }
+ if r.Value == addr.String() {
+ return r.Value, false, nil
+ }
+
+ b.log.Info(
+ "bunny knows this record, attempting update",
+ slog.Group(
+ "bunny",
+ slog.Int64("id", r.ID),
+ slog.Int("type", r.Type),
+ slog.String("name", r.Name),
+ slog.String("value", r.Value),
+ ),
+ )
+
+ // if found, update
+ _, err = request("POST", fmt.Sprintf("%s/%d/records/%d", bunnyAPI, zoneID, r.ID),
+ b.headers(), bytes.NewReader(body))
+ return r.Value, true, err
+ }
+
+ b.log.Info("bunny does not know this record, attempting to create")
+
+ // if not found, create
+ _, err = request("PUT", fmt.Sprintf("%s/%d/records", bunnyAPI, zoneID),
+ b.headers(), bytes.NewReader(body))
+ return "", true, err
+}