summaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorMel <mel@rnrd.eu>2026-08-20 06:18:05 +0200
committerMel <mel@rnrd.eu>2026-08-20 06:18:05 +0200
commitb52c860a7b1d3f57abc442bb8c1257f228d49536 (patch)
tree524865405ba3b972d684ded62d7ab67547bb9040 /source
downloadddnnss-b52c860a7b1d3f57abc442bb8c1257f228d49536.tar.zst
ddnnss-b52c860a7b1d3f57abc442bb8c1257f228d49536.zip
ddnnss
Signed-off-by: Mel <mel@rnrd.eu>
Diffstat (limited to 'source')
-rw-r--r--source/file.go59
-rw-r--r--source/source.go91
-rw-r--r--source/static.go44
-rw-r--r--source/url.go49
4 files changed, 243 insertions, 0 deletions
diff --git a/source/file.go b/source/file.go
new file mode 100644
index 0000000..4ef0e7c
--- /dev/null
+++ b/source/file.go
@@ -0,0 +1,59 @@
+package source
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+ "net/netip"
+ "os"
+)
+
+// a source that reads the address from a file path.
+// useful when your record has to be in-sync with
+// some configured address on your machine.
+type fileSource struct {
+ log *slog.Logger
+ path string
+ family Family
+}
+
+var _ Source = (*fileSource)(nil)
+
+func newFile(log *slog.Logger, cfg Config) (*fileSource, error) {
+ log = log.With(slog.String("source", "file"))
+
+ if _, err := os.Stat(cfg.Path); errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("cannot access file for source: %w", err)
+ }
+
+ return &fileSource{log: log, path: cfg.Path, family: cfg.Family}, nil
+}
+
+func (s *fileSource) Supports(f Family) bool { return f == s.family }
+
+func (s *fileSource) Lookup(Family) (netip.Addr, error) {
+ s.log.Info(
+ "will look up address from file source",
+ slog.String("path", s.path),
+ slog.String("family", string(s.family)),
+ )
+
+ raw, err := os.ReadFile(s.path)
+ if err != nil {
+ return netip.Addr{}, fmt.Errorf("file source read failed: %w", err)
+ }
+ content := string(raw)
+
+ addr, err := parse(content, s.family)
+ if err != nil {
+ s.log.Error(
+ "source file does not contain address of expected family",
+ slog.String("path", s.path),
+ slog.String("expected", string(s.family)),
+ slog.String("content", content),
+ )
+ return netip.Addr{}, fmt.Errorf("source file contains bad content: %w", err)
+ }
+
+ return addr, nil
+}
diff --git a/source/source.go b/source/source.go
new file mode 100644
index 0000000..6fe139c
--- /dev/null
+++ b/source/source.go
@@ -0,0 +1,91 @@
+package source
+
+import (
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "net/netip"
+ "strings"
+ "time"
+)
+
+type Kind string
+
+const (
+ // only the simple generic sources for now!
+ KindStatic Kind = "static"
+ KindURL Kind = "url"
+ KindFile Kind = "file"
+)
+
+type Family string
+
+const (
+ IPv4 Family = "ipv4"
+ IPv6 Family = "ipv6"
+)
+
+type Config struct {
+ Kind Kind `json:"kind"`
+ Family Family `json:"family,omitempty"`
+
+ URL string `json:"url,omitempty"`
+ Path string `json:"path,omitempty"`
+ Value string `json:"value,omitempty"`
+}
+
+type Source interface {
+ Supports(Family) bool
+ Lookup(Family) (netip.Addr, error)
+}
+
+func New(log *slog.Logger, cfg Config) (Source, error) {
+ if cfg.Family != IPv4 && cfg.Family != IPv6 {
+ return nil, fmt.Errorf("%s source needs family set to ipv4 or ipv6", cfg.Kind)
+ }
+
+ if cfg.Kind == KindStatic {
+ return newStatic(log, cfg)
+ }
+
+ if cfg.Kind == KindURL {
+ return newURL(log, cfg)
+ }
+
+ if cfg.Kind == KindFile {
+ return newFile(log, cfg)
+ }
+
+ return nil, fmt.Errorf("unknown source kind %q", cfg.Kind)
+}
+
+// parsing function for ipv4 and ipv6 strings
+func parse(raw string, family Family) (netip.Addr, error) {
+ addr, err := netip.ParseAddr(strings.TrimSpace(raw))
+ if err != nil {
+ return netip.Addr{}, err
+ }
+ if addr.Is6() != (family == IPv6) {
+ return netip.Addr{}, fmt.Errorf("%s is not an %s address", addr, family)
+ }
+ return addr, nil
+}
+
+// generic http fetching helper since a lot of sources will need it.
+func fetch(url string, family Family) (netip.Addr, error) {
+ client := http.Client{Timeout: 15 * time.Second}
+ resp, err := client.Get(url)
+ if err != nil {
+ return netip.Addr{}, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return netip.Addr{}, fmt.Errorf("%s: %s", url, resp.Status)
+ }
+ raw, err := io.ReadAll(io.LimitReader(resp.Body, 256))
+ if err != nil {
+ return netip.Addr{}, err
+ }
+ return parse(string(raw), family)
+}
diff --git a/source/static.go b/source/static.go
new file mode 100644
index 0000000..d2645d6
--- /dev/null
+++ b/source/static.go
@@ -0,0 +1,44 @@
+package source
+
+import (
+ "fmt"
+ "log/slog"
+ "net/netip"
+)
+
+// a simple source that returns the configured address.
+// useful when a record needs to have one specific value,
+// that is known in advance, and for debugging.
+type staticSource struct {
+ log *slog.Logger
+ addr netip.Addr
+}
+
+var _ Source = (*staticSource)(nil)
+
+func newStatic(log *slog.Logger, cfg Config) (*staticSource, error) {
+ log = log.With("source", "static")
+ addr, err := parse(cfg.Value, cfg.Family)
+ if err != nil {
+ log.Error(
+ "static source can't be created from value",
+ slog.String("family", string(cfg.Family)),
+ slog.String("value", cfg.Value),
+ )
+ return nil, fmt.Errorf("can't create static source from value: %w", err)
+ }
+
+ return &staticSource{log: log, addr: addr}, nil
+}
+
+func (s *staticSource) Supports(f Family) bool {
+ return (s.addr.Is4() && f == IPv4) || (s.addr.Is6() && f == IPv6)
+}
+
+func (s *staticSource) Lookup(Family) (netip.Addr, error) {
+ s.log.Info(
+ "looked up address from static source",
+ slog.String("addr", s.addr.String()),
+ )
+ return s.addr, nil
+}
diff --git a/source/url.go b/source/url.go
new file mode 100644
index 0000000..067d48b
--- /dev/null
+++ b/source/url.go
@@ -0,0 +1,49 @@
+package source
+
+import (
+ "fmt"
+ "log/slog"
+ "net/netip"
+ nurl "net/url"
+)
+
+// a simple source that fetches an address from a url.
+// sends a http get request to url, and parses whatever it found there.
+// can be used to fetch address values from generic "my ip"
+// services which don't have direct support.
+// note that the address does have to be returned in plain-text!
+type urlSource struct {
+ log *slog.Logger
+ url *nurl.URL
+ family Family
+}
+
+var _ Source = (*urlSource)(nil)
+
+func newURL(log *slog.Logger, cfg Config) (*urlSource, error) {
+ log = log.With("source", "url")
+
+ url, err := nurl.ParseRequestURI(cfg.URL)
+ if err != nil {
+ return nil, fmt.Errorf("invalid url for source: %w", err)
+ }
+
+ return &urlSource{log: log, url: url, family: cfg.Family}, nil
+}
+
+func (s *urlSource) Supports(f Family) bool { return f == s.family }
+
+func (s *urlSource) Lookup(Family) (netip.Addr, error) {
+ s.log.Info(
+ "will look up address via fetching from url",
+ slog.String("url", s.url.String()),
+ slog.String("family", string(s.family)),
+ )
+
+ addr, err := fetch(s.url.String(), s.family)
+ if err != nil {
+ return netip.Addr{}, fmt.Errorf("failed to source address from url: %w", err)
+ }
+
+ return addr, nil
+}