From a4b921e75f57d2083d256036f6d28a58bb4cf075 Mon Sep 17 00:00:00 2001 From: 0xFelix Date: Sat, 18 Apr 2026 18:28:45 +0200 Subject: [PATCH 1/2] config: make enabled endpoints configurable Add an Endpoints struct to Config with bool fields for each endpoint group (plain, nic, acmedns, httpreq, directadmin). All endpoints are enabled by default. A custom UnmarshalYAML implementation ensures that explicitly listing endpoints in YAML disables all others, matching the env var semantics of ENDPOINTS=plain,nic. Also extract envRateLimit and envLockout helpers to stay within the cyclomatic complexity limit. Signed-off-by: Felix Matouschek Signed-off-by: 0xFelix --- pkg/app/app.go | 46 ++++++++++------- pkg/config/config.go | 97 +++++++++++++++++++++++++++++++----- pkg/config/config_test.go | 1 + tests/libserver/libserver.go | 2 + 4 files changed, 115 insertions(+), 31 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 85313a5..293e30e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -42,24 +42,34 @@ func New(cfg *config.Config) http.Handler { rl := middleware.NewRateLimit(limiter, middleware.RateLimitExceeded) mux := http.NewServeMux() - mux.Handle("GET /plain/update", - handle(cfg, rl, middleware.BindPlain, authorizer, updater, middleware.StatusOk)) - mux.Handle("GET /nic/update", handle( - cfg, middleware.NewRateLimit(limiter, middleware.NicRateLimitExceeded), middleware.BindNicUpdate, - middleware.NicAuth(cfg, lockout), middleware.NicUpdate(updater), middleware.StatusOkNicUpdate, - )) - mux.Handle("POST /acmedns/update", - handle(cfg, rl, middleware.BindAcmeDNS, authorizer, updater, middleware.StatusOkAcmeDNS)) - mux.Handle("POST /httpreq/present", - handle(cfg, rl, middleware.ContentTypeJSON, middleware.BindHTTPReq, authorizer, updater, middleware.StatusOk)) - mux.Handle("POST /httpreq/cleanup", - handle(cfg, rl, middleware.ContentTypeJSON, middleware.BindHTTPReq, authorizer, cleaner, middleware.StatusOk)) - mux.Handle("GET /directadmin/CMD_API_SHOW_DOMAINS", - handle(cfg, rl, middleware.NewShowDomainsDirectAdmin(cfg, lockout))) - mux.Handle("GET /directadmin/CMD_API_DOMAIN_POINTER", - handle(cfg, rl, middleware.StatusOk)) - mux.Handle("GET /directadmin/CMD_API_DNS_CONTROL", - handle(cfg, rl, middleware.BindDirectAdmin, authorizer, updater, middleware.StatusOkDirectAdmin)) + if cfg.Endpoints.Plain { + mux.Handle("GET /plain/update", + handle(cfg, rl, middleware.BindPlain, authorizer, updater, middleware.StatusOk)) + } + if cfg.Endpoints.Nic { + mux.Handle("GET /nic/update", handle( + cfg, middleware.NewRateLimit(limiter, middleware.NicRateLimitExceeded), middleware.BindNicUpdate, + middleware.NicAuth(cfg, lockout), middleware.NicUpdate(updater), middleware.StatusOkNicUpdate, + )) + } + if cfg.Endpoints.AcmeDNS { + mux.Handle("POST /acmedns/update", + handle(cfg, rl, middleware.BindAcmeDNS, authorizer, updater, middleware.StatusOkAcmeDNS)) + } + if cfg.Endpoints.HTTPReq { + mux.Handle("POST /httpreq/present", + handle(cfg, rl, middleware.ContentTypeJSON, middleware.BindHTTPReq, authorizer, updater, middleware.StatusOk)) + mux.Handle("POST /httpreq/cleanup", + handle(cfg, rl, middleware.ContentTypeJSON, middleware.BindHTTPReq, authorizer, cleaner, middleware.StatusOk)) + } + if cfg.Endpoints.DirectAdmin { + mux.Handle("GET /directadmin/CMD_API_SHOW_DOMAINS", + handle(cfg, rl, middleware.NewShowDomainsDirectAdmin(cfg, lockout))) + mux.Handle("GET /directadmin/CMD_API_DOMAIN_POINTER", + handle(cfg, rl, middleware.StatusOk)) + mux.Handle("GET /directadmin/CMD_API_DNS_CONTROL", + handle(cfg, rl, middleware.BindDirectAdmin, authorizer, updater, middleware.StatusOkDirectAdmin)) + } return mux } diff --git a/pkg/config/config.go b/pkg/config/config.go index d7dda1b..df0af91 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,7 +16,7 @@ type AllowedDomains map[string][]*net.IPNet func (out *AllowedDomains) FromString(val string) error { allowedDomains := AllowedDomains{} - for _, part := range strings.Split(val, ";") { + for part := range strings.SplitSeq(val, ";") { parts := strings.Split(part, ",") const expectedParts = 2 @@ -41,6 +41,7 @@ type Config struct { Token string `yaml:"token"` Timeout int `yaml:"timeout"` Auth Auth `yaml:"auth"` + Endpoints Endpoints `yaml:"endpoints"` RecordTTL int `yaml:"recordTTL"` ListenAddr string `yaml:"listenAddr"` TrustedProxies []string `yaml:"trustedProxies"` @@ -50,12 +51,38 @@ type Config struct { Debug bool `yaml:"debug"` } +type Endpoints struct { + Plain bool `yaml:"plain"` + Nic bool `yaml:"nic"` + AcmeDNS bool `yaml:"acmedns"` + HTTPReq bool `yaml:"httpreq"` + DirectAdmin bool `yaml:"directadmin"` +} + +func (e *Endpoints) UnmarshalYAML(unmarshal func(any) error) error { + type raw Endpoints + var r raw + if err := unmarshal(&r); err != nil { + return err + } + *e = Endpoints(r) + return nil +} + type Auth struct { Method string `yaml:"method"` AllowedDomains AllowedDomains `yaml:"allowedDomains"` Users []User `yaml:"users"` } +const ( + EndpointPlain = "plain" + EndpointNic = "nic" + EndpointAcmeDNS = "acmedns" + EndpointHTTPReq = "httpreq" + EndpointDirectAdmin = "directadmin" +) + const ( AuthMethodAllowedDomains = "allowedDomains" AuthMethodUsers = "users" @@ -87,6 +114,13 @@ func NewConfig() *Config { Auth: Auth{ Method: AuthMethodBoth, }, + Endpoints: Endpoints{ + Plain: true, + Nic: true, + AcmeDNS: true, + HTTPReq: true, + DirectAdmin: true, + }, RecordTTL: 60, ListenAddr: ":8081", RateLimit: RateLimit{ @@ -140,22 +174,13 @@ func ParseEnv() (*Config, error) { if err := envBool("DEBUG", &cfg.Debug); err != nil { return nil, err } - if err := envFloat("RATE_LIMIT_RPS", &cfg.RateLimit.RPS); err != nil { - return nil, err - } - if err := envInt("RATE_LIMIT_BURST", &cfg.RateLimit.Burst); err != nil { - return nil, err - } - if err := envInt("RATE_LIMIT_IDLE_SECONDS", &cfg.RateLimit.IdleSeconds); err != nil { - return nil, err - } - if err := envInt("LOCKOUT_MAX_ATTEMPTS", &cfg.Lockout.MaxAttempts); err != nil { + if err := envRateLimit(&cfg.RateLimit); err != nil { return nil, err } - if err := envInt("LOCKOUT_DURATION_SECONDS", &cfg.Lockout.DurationSeconds); err != nil { + if err := envLockout(&cfg.Lockout); err != nil { return nil, err } - if err := envInt("LOCKOUT_WINDOW_SECONDS", &cfg.Lockout.WindowSeconds); err != nil { + if err := envEndpoints(&cfg.Endpoints); err != nil { return nil, err } @@ -215,6 +240,52 @@ func envBool(key string, dst *bool) error { return nil } +func envRateLimit(rl *RateLimit) error { + if err := envFloat("RATE_LIMIT_RPS", &rl.RPS); err != nil { + return err + } + if err := envInt("RATE_LIMIT_BURST", &rl.Burst); err != nil { + return err + } + return envInt("RATE_LIMIT_IDLE_SECONDS", &rl.IdleSeconds) +} + +func envLockout(l *Lockout) error { + if err := envInt("LOCKOUT_MAX_ATTEMPTS", &l.MaxAttempts); err != nil { + return err + } + if err := envInt("LOCKOUT_DURATION_SECONDS", &l.DurationSeconds); err != nil { + return err + } + return envInt("LOCKOUT_WINDOW_SECONDS", &l.WindowSeconds) +} + +func envEndpoints(endpoints *Endpoints) error { + v, ok := os.LookupEnv("ENDPOINTS") + if !ok { + return nil + } + *endpoints = Endpoints{} + for name := range strings.SplitSeq(v, ",") { + name = strings.TrimSpace(name) + switch name { + case EndpointPlain: + endpoints.Plain = true + case EndpointNic: + endpoints.Nic = true + case EndpointAcmeDNS: + endpoints.AcmeDNS = true + case EndpointHTTPReq: + endpoints.HTTPReq = true + case EndpointDirectAdmin: + endpoints.DirectAdmin = true + default: + return fmt.Errorf("invalid endpoint %q in ENDPOINTS", name) + } + } + return nil +} + func envTrustedProxies(cfg *Config) { v, ok := os.LookupEnv("TRUSTED_PROXIES") if !ok { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f098e70..e15a420 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -278,6 +278,7 @@ var _ = Describe("Config", func() { AllowedDomains: allowedDomains, Users: users, }, + Endpoints: config.Endpoints{Plain: true, Nic: true, AcmeDNS: true, HTTPReq: true, DirectAdmin: true}, RecordTTL: recordTTL, ListenAddr: listenAddr, TrustedProxies: trustedProxies, diff --git a/tests/libserver/libserver.go b/tests/libserver/libserver.go index adce634..e95f0b1 100644 --- a/tests/libserver/libserver.go +++ b/tests/libserver/libserver.go @@ -36,6 +36,7 @@ func New(url string, ttl int) (server *httptest.Server, token, username, passwor Domains: []string{"*"}, }}, }, + Endpoints: config.Endpoints{Plain: true, Nic: true, AcmeDNS: true, HTTPReq: true, DirectAdmin: true}, RecordTTL: ttl, RateLimit: config.RateLimit{RPS: 1000, Burst: 1000, IdleSeconds: 600}, Lockout: config.Lockout{MaxAttempts: 1000, DurationSeconds: 3600, WindowSeconds: 900}, @@ -50,6 +51,7 @@ func NewNoAllowedDomains(url string) *httptest.Server { Auth: config.Auth{ Method: config.AuthMethodAllowedDomains, }, + Endpoints: config.Endpoints{Plain: true, Nic: true, AcmeDNS: true, HTTPReq: true, DirectAdmin: true}, RateLimit: config.RateLimit{RPS: 1000, Burst: 1000, IdleSeconds: 600}, Lockout: config.Lockout{MaxAttempts: 1000, DurationSeconds: 3600, WindowSeconds: 900}, } From be341e441072ec17093c79a5c65b701d7038b1a8 Mon Sep 17 00:00:00 2001 From: 0xFelix Date: Sat, 18 Apr 2026 18:38:05 +0200 Subject: [PATCH 2/2] config: log enabled endpoints on startup Signed-off-by: Felix Matouschek Signed-off-by: 0xFelix --- main.go | 2 ++ pkg/config/config.go | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/main.go b/main.go index 2035644..ec2c61b 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -33,6 +34,7 @@ func main() { if err != nil { log.Fatal(err) } + log.Printf("Enabled endpoints: %s", strings.Join(cfg.Endpoints.Enabled(), ", ")) log.Printf("Authorization method set to: %s", cfg.Auth.Method) log.Printf("Starting hetzner-dnsapi-proxy, listening on %s", cfg.ListenAddr) if err := runServer(cfg.ListenAddr, app.New(cfg)); err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index df0af91..d10869f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -59,6 +59,26 @@ type Endpoints struct { DirectAdmin bool `yaml:"directadmin"` } +func (e *Endpoints) Enabled() []string { + var names []string + if e.Plain { + names = append(names, EndpointPlain) + } + if e.Nic { + names = append(names, EndpointNic) + } + if e.AcmeDNS { + names = append(names, EndpointAcmeDNS) + } + if e.HTTPReq { + names = append(names, EndpointHTTPReq) + } + if e.DirectAdmin { + names = append(names, EndpointDirectAdmin) + } + return names +} + func (e *Endpoints) UnmarshalYAML(unmarshal func(any) error) error { type raw Endpoints var r raw