Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## Project Overview

hetzner-dnsapi-proxy is a Go HTTP server that proxies DNS update requests
to the Hetzner DNS API or Hetzner Cloud API. It supports multiple DNS API
protocols (lego HTTP request, ACMEDNS, DirectAdmin Legacy, plain HTTP).
to the Hetzner Cloud API. It supports multiple DNS API protocols (lego
HTTP request, ACMEDNS, DirectAdmin Legacy, plain HTTP).

## Building and Testing

Expand Down Expand Up @@ -41,9 +41,9 @@ Run `make vendor` after modifying dependencies.
- `pkg/app/` - HTTP router setup
- `pkg/config/` - Configuration (file and env parsing)
- `pkg/middleware/` - HTTP middleware (auth, domain checks, DNS update/cleanup)
- `pkg/middleware/update/` - Record creation (dns/ and cloud/ backends)
- `pkg/middleware/clean/` - Record cleanup (dns/ and cloud/ backends)
- `pkg/hetzner/` - Hetzner API clients (DNS API and Cloud API)
- `pkg/middleware/update/` - Record creation (cloud/ backend)
- `pkg/middleware/clean/` - Record cleanup (cloud/ backend)
- `pkg/hetzner/` - Hetzner Cloud API client helpers
- `pkg/data/` - Shared data types
- `pkg/sanitize/` - Input sanitization

Expand Down
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# hetzner-dnsapi-proxy

hetzner-dnsapi-proxy proxies DNS API update requests to the [Hetzner](https://dns.hetzner.com/api-docs) DNS API.
hetzner-dnsapi-proxy proxies DNS API update requests to the [Hetzner Cloud API](https://docs.hetzner.cloud).

> **Note:** Support for the old Hetzner DNS API has been removed since it has
> been [shut down](https://docs.hetzner.com/networking/dns/faq/beta/#timeline).
> If upgrading from a setup that used the old DNS API, update your `API_TOKEN`
> (or `token` in the config file) to a Hetzner Cloud API token. The `cloudAPI`
> config option and `CLOUD_API` environment variable are no longer recognized
> and can be removed from existing configurations.

## Container image

Expand All @@ -10,17 +17,11 @@ Get the container image from [ghcr.io](https://github.com/0xFelix/hetzner-dnsapi

| API | Endpoint |
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| lego HTTP request | POST `/httpreq/present`<br>POST `/httpreq/cleanup` (only deletes records when the Cloud API is enabled, otherwise returns `200 OK`)<br>(see https://go-acme.github.io/lego/dns/httpreq/) |
| ACMEDNS | POST `/acmedns/update`<br>(see https://github.com/joohoi/acme-dns#update-endpoint) |
| lego HTTP request | POST `/httpreq/present`<br>POST `/httpreq/cleanup`<br>(see https://go-acme.github.io/lego/dns/httpreq/) |
| ACMEDNS | POST `/acmedns/update`<br>(see https://github.com/joohoi/acme-dns#update-endpoint) |
| DirectAdmin Legacy | GET `/directadmin/CMD_API_SHOW_DOMAINS`<br>GET `/directadmin/CMD_API_DNS_CONTROL` (only adding A/AAAA/TXT records, everything else always returns `200 OK`)<br>GET `/directadmin/CMD_API_DOMAIN_POINTER` (only a stub, always returns `200 OK`)<br>(see https://docs.directadmin.com/developer/api/legacy-api.html and https://www.directadmin.com/features.php?id=504) |
| plain HTTP | GET `/plain/update` (query params `hostname` and `ip` (can be ipv4 for A or ipv6 for AAAA records), if auth method is `users` then HTTP Basic auth is used) <br/> |

## Hetzner Cloud API Support

This proxy can use the Hetzner DNS API (default) or the Hetzner Cloud API. The Cloud API is more modern and allows for deleting records, which is used by the `/httpreq/cleanup` endpoint. When using the DNS API, the cleanup endpoint will not delete any records.

It is recommended to use the Cloud API for new setups. You can enable it by setting the `cloudAPI` option in the configuration file or the `CLOUD_API` environment variable.

## Configuration

Configuration can be passed by environment variables or from a file (with
Expand Down Expand Up @@ -67,19 +68,17 @@ listenAddr: :8081
trustedProxies:
- 127.0.0.1
debug: false
cloudAPI: false
```

### Environment variables

| Variable | Type | Description | Required | Default |
|:------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------------|
| `API_BASE_URL` | string | Base URL of the API. Defaults to `https://dns.hetzner.com/api/v1` for DNS API or `https://api.hetzner.cloud/v1` when `CLOUD_API` is `true`. | n | (Depends on API selection) |
| `API_BASE_URL` | string | Base URL of the API | N | `https://api.hetzner.cloud/v1` |
| `API_TOKEN` | string | Auth token for the API | Y | |
| `API_TIMEOUT` | int | Timeout for calls to the API in seconds | N | 15 seconds |
| `RECORD_TTL` | int | TTL that is set when creating/updating records | N | 60 seconds |
| `ALLOWED_DOMAINS` | string | Combination of domains and CIDRs allowed to update them, example:<br>`example1.com,127.0.0.1/32;_acme-challenge.example2.com,127.0.0.1/32` | Y | |
| `LISTEN_ADDR` | string | Listen address of hetzner-dnsapi-proxy | N | `:8081` |
| `TRUSTED_PROXIES` | string | List of trusted proxy host addresses separated by comma | N | Trust all proxies |
| `CLOUD_API` | bool | Use the Hetzner Cloud API instead of the DNS API. | N | `false` |
| `DEBUG` | bool | Output debug logs of received requests | N | `false` |
7 changes: 0 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,6 @@ func main() {
log.Fatal(err)
}
log.Printf("Authorization method set to: %s", cfg.Auth.Method)

if cfg.CloudAPI {
log.Println("Running in Cloud API mode")
} else {
log.Println("Running in DNS API mode")
}

log.Printf("Starting hetzner-dnsapi-proxy, listening on %s", cfg.ListenAddr)
if err := runServer(cfg.ListenAddr, app.New(cfg)); err != nil {
log.Fatal("Error running server:", err)
Expand Down
16 changes: 1 addition & 15 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ type Config struct {
ListenAddr string `yaml:"listenAddr"`
TrustedProxies []string `yaml:"trustedProxies"`
Debug bool `yaml:"debug"`
CloudAPI bool `yaml:"cloudAPI"`
}

type Auth struct {
Expand Down Expand Up @@ -78,19 +77,10 @@ func NewConfig() *Config {
}
}

//nolint:gocyclo
func ParseEnv() (*Config, error) {
cfg := NewConfig()
cfg.Auth.Method = AuthMethodAllowedDomains

if cloudAPI, ok := os.LookupEnv("CLOUD_API"); ok {
cloudAPIBool, err := strconv.ParseBool(cloudAPI)
if err != nil {
return nil, fmt.Errorf("failed to parse CLOUD_API: %v", err)
}
cfg.CloudAPI = cloudAPIBool
}

if baseURL, ok := os.LookupEnv("API_BASE_URL"); ok {
cfg.BaseURL = baseURL
}
Expand Down Expand Up @@ -198,11 +188,7 @@ func AuthMethodIsValid(authMethod string) bool {

func setDefaultBaseURL(c *Config) {
if c.BaseURL == "" {
if c.CloudAPI {
c.BaseURL = "https://api.hetzner.cloud/v1"
} else {
c.BaseURL = "https://dns.hetzner.com/api/v1"
}
c.BaseURL = "https://api.hetzner.cloud/v1"
}
}

Expand Down
11 changes: 0 additions & 11 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ var _ = Describe("Config", func() {
listenAddr = "127.0.0.1:8080"
trustedProxiesStr = "127.0.0.1,192.168.0.1,192.168.0.2"
debugStr = "true"
cloudAPIStr = "true"
)

var (
Expand All @@ -127,7 +126,6 @@ var _ = Describe("Config", func() {
envListenAddr = "LISTEN_ADDR"
envTrustedProxies = "TRUSTED_PROXIES"
envDebug = "DEBUG"
envCloudAPI = "CLOUD_API"
)

BeforeEach(func() {
Expand All @@ -150,7 +148,6 @@ var _ = Describe("Config", func() {
Expect(os.Unsetenv(envListenAddr)).To(Succeed())
Expect(os.Unsetenv(envTrustedProxies)).To(Succeed())
Expect(os.Unsetenv(envDebug)).To(Succeed())
Expect(os.Unsetenv(envCloudAPI)).To(Succeed())
})

It("should parse environment successfully", func() {
Expand All @@ -162,7 +159,6 @@ var _ = Describe("Config", func() {
Expect(os.Setenv(envListenAddr, listenAddr)).To(Succeed())
Expect(os.Setenv(envTrustedProxies, trustedProxiesStr)).To(Succeed())
Expect(os.Setenv(envDebug, debugStr)).To(Succeed())
Expect(os.Setenv(envCloudAPI, cloudAPIStr)).To(Succeed())

cfg, err := config.ParseEnv()
Expect(err).ToNot(HaveOccurred())
Expand All @@ -177,7 +173,6 @@ var _ = Describe("Config", func() {
Expect(cfg.ListenAddr).To(Equal(listenAddr))
Expect(cfg.TrustedProxies).To(Equal(trustedProxies))
Expect(cfg.Debug).To(BeTrue())
Expect(cfg.CloudAPI).To(BeTrue())
})

DescribeTable("should fail on invalid environment variables", func(setEnv func(), errMsg string) {
Expand All @@ -204,11 +199,6 @@ var _ = Describe("Config", func() {
Expect(os.Setenv(envAllowedDomains, allowedDomainsStr)).To(Succeed())
Expect(os.Setenv(envDebug, "something")).To(Succeed())
}, "failed to parse DEBUG: strconv.ParseBool: parsing \"something\": invalid syntax"),
Entry("CLOUD_API not a bool", func() {
Expect(os.Setenv(envAPIToken, apiToken)).To(Succeed())
Expect(os.Setenv(envAllowedDomains, allowedDomainsStr)).To(Succeed())
Expect(os.Setenv(envCloudAPI, "something")).To(Succeed())
}, "failed to parse CLOUD_API: strconv.ParseBool: parsing \"something\": invalid syntax"),
)
})

Expand Down Expand Up @@ -253,7 +243,6 @@ var _ = Describe("Config", func() {
ListenAddr: listenAddr,
TrustedProxies: trustedProxies,
Debug: true,
CloudAPI: true,
}

data, err := yaml.Marshal(cfg)
Expand Down
23 changes: 0 additions & 23 deletions pkg/hetzner/hetzner.go

This file was deleted.

12 changes: 1 addition & 11 deletions pkg/middleware/clean/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,10 @@ import (
"github.com/0xfelix/hetzner-dnsapi-proxy/pkg/config"
"github.com/0xfelix/hetzner-dnsapi-proxy/pkg/data"
"github.com/0xfelix/hetzner-dnsapi-proxy/pkg/middleware/clean/cloud"
"github.com/0xfelix/hetzner-dnsapi-proxy/pkg/middleware/clean/dns"
)

type cleaner interface {
Clean(context.Context, *data.ReqData) error
}

func New(cfg *config.Config, m *sync.Mutex) func(http.Handler) http.Handler {
var c cleaner
if cfg.CloudAPI {
c = cloud.New(cfg, m)
} else {
c = dns.New()
}
c := cloud.New(cfg, m)

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
17 changes: 0 additions & 17 deletions pkg/middleware/clean/dns/dns.go

This file was deleted.

Loading
Loading