feat(cert): add registry-based DNS provider system with 21 libdns providers
Refs #37
This commit is contained in:
@@ -63,6 +63,7 @@ Legacy single-panel config is fully compatible. Appending bindings auto-migrates
|
|||||||
|
|
||||||
- Custom routes: [docs-custom-routes.md](docs-custom-routes.md)
|
- Custom routes: [docs-custom-routes.md](docs-custom-routes.md)
|
||||||
- Custom outbounds: [docs-custom-outbounds.md](docs-custom-outbounds.md)
|
- Custom outbounds: [docs-custom-outbounds.md](docs-custom-outbounds.md)
|
||||||
|
- DNS providers (ACME DNS-01): [docs-dns-providers.md](docs-dns-providers.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# DNS Providers
|
||||||
|
|
||||||
|
Used by `cert_config.cert_mode: "dns"` for ACME DNS-01.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cert_config:
|
||||||
|
cert_mode: "dns"
|
||||||
|
dns_provider: "cloudflare"
|
||||||
|
dns_env:
|
||||||
|
CLOUDFLARE_DNS_API_TOKEN: "xxx"
|
||||||
|
```
|
||||||
|
|
||||||
|
| provider | aliases | env |
|
||||||
|
|---|---|---|
|
||||||
|
| cloudflare | cf | `CLOUDFLARE_DNS_API_TOKEN` |
|
||||||
|
| alidns | aliyun | `ALICLOUD_ACCESS_KEY_ID`, `ALICLOUD_ACCESS_KEY_SECRET` |
|
||||||
|
| tencentcloud | tencent | `TENCENTCLOUD_SECRET_ID`, `TENCENTCLOUD_SECRET_KEY` |
|
||||||
|
| route53 | aws | AWS credential chain (env / IAM role / profile) |
|
||||||
|
| godaddy | | `GODADDY_API_KEY`, `GODADDY_API_SECRET` |
|
||||||
|
| namecheap | | `NAMECHEAP_API_KEY`, `NAMECHEAP_API_USER` |
|
||||||
|
| namesilo | | `NAMESILO_API_TOKEN` |
|
||||||
|
| digitalocean | do | `DO_AUTH_TOKEN` |
|
||||||
|
| linode | | `LINODE_TOKEN` |
|
||||||
|
| hetzner | | `HETZNER_API_TOKEN` |
|
||||||
|
| gandi | | `GANDI_BEARER_TOKEN` |
|
||||||
|
| porkbun | | `PORKBUN_API_KEY`, `PORKBUN_API_SECRET_KEY` |
|
||||||
|
| netlify | | `NETLIFY_PERSONAL_ACCESS_TOKEN` |
|
||||||
|
| azure | | `AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP_NAME` |
|
||||||
|
| googleclouddns | gcp, gcloud | `GCE_PROJECT` |
|
||||||
|
| huaweicloud | huawei | `HUAWEICLOUD_ACCESS_KEY_ID`, `HUAWEICLOUD_SECRET_ACCESS_KEY` |
|
||||||
|
| bunny | | `BUNNY_ACCESS_KEY` |
|
||||||
|
| duckdns | | `DUCKDNS_TOKEN` |
|
||||||
|
| ovh | | `OVH_APPLICATION_KEY`, `OVH_APPLICATION_SECRET`, `OVH_CONSUMER_KEY` |
|
||||||
|
| vultr | | `VULTR_API_TOKEN` |
|
||||||
|
| desec | | `DESEC_TOKEN` |
|
||||||
@@ -8,18 +8,48 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/libdns/alidns v1.0.6
|
github.com/libdns/alidns v1.0.6
|
||||||
|
github.com/libdns/azure v0.5.0
|
||||||
|
github.com/libdns/bunny v1.6.1
|
||||||
github.com/libdns/cloudflare v0.2.2
|
github.com/libdns/cloudflare v0.2.2
|
||||||
|
github.com/libdns/desec v1.1.1
|
||||||
|
github.com/libdns/digitalocean v0.0.0-20250606071607-dfa7af5c2e31
|
||||||
|
github.com/libdns/duckdns v0.3.0
|
||||||
|
github.com/libdns/gandi v1.1.0
|
||||||
|
github.com/libdns/godaddy v1.1.0
|
||||||
|
github.com/libdns/googleclouddns v1.2.0
|
||||||
|
github.com/libdns/hetzner v1.0.0
|
||||||
|
github.com/libdns/huaweicloud v1.0.1
|
||||||
|
github.com/libdns/linode v0.5.0
|
||||||
|
github.com/libdns/namecheap v1.0.0
|
||||||
|
github.com/libdns/namesilo v1.0.0
|
||||||
|
github.com/libdns/netlify v1.2.0
|
||||||
|
github.com/libdns/ovh v1.1.0
|
||||||
|
github.com/libdns/porkbun v1.1.0
|
||||||
|
github.com/libdns/route53 v1.6.0
|
||||||
|
github.com/libdns/tencentcloud v1.4.3
|
||||||
|
github.com/libdns/vultr/v2 v2.0.4
|
||||||
github.com/sagernet/sing v0.8.2
|
github.com/sagernet/sing v0.8.2
|
||||||
github.com/sagernet/sing-box v1.13.2
|
github.com/sagernet/sing-box v1.13.2
|
||||||
github.com/shirou/gopsutil/v4 v4.26.2
|
github.com/shirou/gopsutil/v4 v4.26.2
|
||||||
github.com/xtls/xray-core v1.260327.0
|
github.com/xtls/xray-core v1.260327.0
|
||||||
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/term v0.41.0
|
golang.org/x/term v0.41.0
|
||||||
golang.org/x/time v0.12.0
|
golang.org/x/time v0.12.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cloud.google.com/go/auth v0.16.5 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||||
@@ -27,7 +57,23 @@ require (
|
|||||||
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
||||||
github.com/anytls/sing-anytls v0.0.11 // indirect
|
github.com/anytls/sing-anytls v0.0.11 // indirect
|
||||||
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
|
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.0 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cilium/ebpf v0.21.0 // indirect
|
github.com/cilium/ebpf v0.21.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
@@ -36,8 +82,10 @@ require (
|
|||||||
github.com/database64128/netx-go v0.1.1 // indirect
|
github.com/database64128/netx-go v0.1.1 // indirect
|
||||||
github.com/database64128/tfo-go/v2 v2.3.2 // indirect
|
github.com/database64128/tfo-go/v2 v2.3.2 // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
|
github.com/digitalocean/godo v1.148.0 // indirect
|
||||||
github.com/ebitengine/purego v0.10.0 // indirect
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
github.com/enfein/mieru/v3 v3.29.0 // indirect
|
github.com/enfein/mieru/v3 v3.29.0 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
|
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/gaissmai/bart v0.18.0 // indirect
|
github.com/gaissmai/bart v0.18.0 // indirect
|
||||||
@@ -45,38 +93,67 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
github.com/go-chi/render v1.0.3 // indirect
|
github.com/go-chi/render v1.0.3 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.19.16 // indirect
|
||||||
|
github.com/go-openapi/errors v0.19.9 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/loads v0.20.0 // indirect
|
||||||
|
github.com/go-openapi/runtime v0.19.24 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.0 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.19.11 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.12 // indirect
|
||||||
|
github.com/go-openapi/validate v0.20.0 // indirect
|
||||||
|
github.com/go-resty/resty/v2 v2.16.5 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
github.com/gofrs/uuid/v5 v5.4.0 // indirect
|
github.com/gofrs/uuid/v5 v5.4.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect
|
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||||
github.com/juju/ratelimit v1.0.2 // indirect
|
github.com/juju/ratelimit v1.0.2 // indirect
|
||||||
github.com/keybase/go-keychain v0.0.1 // indirect
|
github.com/keybase/go-keychain v0.0.1 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/libdns/acmedns v0.5.0 // indirect
|
github.com/libdns/acmedns v0.5.0 // indirect
|
||||||
github.com/libdns/libdns v1.1.1 // indirect
|
github.com/libdns/libdns v1.1.1 // indirect
|
||||||
|
github.com/linode/linodego v1.56.0 // indirect
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mdlayher/netlink v1.9.0 // indirect
|
github.com/mdlayher/netlink v1.9.0 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/metacubex/utls v1.8.4 // indirect
|
github.com/metacubex/utls v1.8.4 // indirect
|
||||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||||
github.com/miekg/dns v1.1.72 // indirect
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||||
|
github.com/netlify/open-api/v2 v2.40.0 // indirect
|
||||||
github.com/openai/openai-go/v3 v3.26.0 // indirect
|
github.com/openai/openai-go/v3 v3.26.0 // indirect
|
||||||
|
github.com/ovh/go-ovh v1.7.0 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pires/go-proxyproto v0.11.0 // indirect
|
github.com/pires/go-proxyproto v0.11.0 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
@@ -147,10 +224,17 @@ require (
|
|||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
|
github.com/vultr/govultr/v3 v3.20.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
|
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.4.4 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
@@ -159,7 +243,6 @@ require (
|
|||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.34.0 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
@@ -168,13 +251,16 @@ require (
|
|||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
|
google.golang.org/api v0.249.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/grpc v1.79.3 // indirect
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/sagernet/sing-box => github.com/cedar2025/sing-box v1.14.0-alpha.2.0.20260316103356-2e665cb7e295
|
replace github.com/sagernet/sing-box => github.com/cedar2025/sing-box v1.14.0-alpha.2.0.20260316103356-2e665cb7e295
|
||||||
|
|
||||||
replace github.com/xtls/xray-core => github.com/cedar2025/Xray-core v0.0.0-20260409213332-f47935539965
|
replace github.com/xtls/xray-core => github.com/cedar2025/Xray-core v0.0.0-20260409213332-f47935539965
|
||||||
|
|||||||
+10
-37
@@ -19,9 +19,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/libdns/alidns"
|
|
||||||
"github.com/libdns/cloudflare"
|
|
||||||
|
|
||||||
|
"github.com/cedar2025/xboard-node/internal/cert/dnsproviders"
|
||||||
"github.com/cedar2025/xboard-node/internal/config"
|
"github.com/cedar2025/xboard-node/internal/config"
|
||||||
"github.com/cedar2025/xboard-node/internal/kernel"
|
"github.com/cedar2025/xboard-node/internal/kernel"
|
||||||
"github.com/cedar2025/xboard-node/internal/nlog"
|
"github.com/cedar2025/xboard-node/internal/nlog"
|
||||||
@@ -416,36 +415,20 @@ func (m *Manager) buildDNSSolver() (*certmagic.DNS01Solver, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) newDNSProvider() (certmagic.DNSProvider, error) {
|
func (m *Manager) newDNSProvider() (certmagic.DNSProvider, error) {
|
||||||
name := strings.ToLower(strings.TrimSpace(m.cfg.DNSProvider))
|
name := strings.TrimSpace(m.cfg.DNSProvider)
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("dns_provider is required for cert_mode=dns")
|
||||||
|
}
|
||||||
env := m.cfg.DNSEnv
|
env := m.cfg.DNSEnv
|
||||||
if env == nil {
|
if env == nil {
|
||||||
env = map[string]string{}
|
env = map[string]string{}
|
||||||
}
|
}
|
||||||
|
p, ok := dnsproviders.Get(name)
|
||||||
switch name {
|
if !ok {
|
||||||
case "cloudflare", "cf":
|
return nil, fmt.Errorf("unsupported dns_provider: %q (supported: %s)",
|
||||||
token := firstOf(env, "CF_API_TOKEN", "CLOUDFLARE_API_TOKEN")
|
name, strings.Join(dnsproviders.CanonicalNames(), ", "))
|
||||||
if token == "" {
|
|
||||||
return nil, fmt.Errorf("cloudflare requires CF_API_TOKEN in dns_env")
|
|
||||||
}
|
|
||||||
return &cloudflare.Provider{APIToken: token}, nil
|
|
||||||
|
|
||||||
case "alidns", "aliyun":
|
|
||||||
keyID := firstOf(env, "ALICLOUD_ACCESS_KEY_ID", "ALI_ACCESS_KEY_ID")
|
|
||||||
keySecret := firstOf(env, "ALICLOUD_ACCESS_KEY_SECRET", "ALI_ACCESS_KEY_SECRET")
|
|
||||||
if keyID == "" || keySecret == "" {
|
|
||||||
return nil, fmt.Errorf("alidns requires ALICLOUD_ACCESS_KEY_ID and ALICLOUD_ACCESS_KEY_SECRET in dns_env")
|
|
||||||
}
|
|
||||||
return &alidns.Provider{
|
|
||||||
CredentialInfo: alidns.CredentialInfo{
|
|
||||||
AccessKeyID: keyID,
|
|
||||||
AccessKeySecret: keySecret,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported dns_provider: %q (supported: cloudflare, alidns)", name)
|
|
||||||
}
|
}
|
||||||
|
return p.Build(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Helpers ───────────────────────────────────────────────────────────────
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||||
@@ -478,13 +461,3 @@ func (m *Manager) loadCertFromStorage(ctx context.Context, storage certmagic.Sto
|
|||||||
m.renewed.Store(true)
|
m.renewed.Store(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// firstOf returns the first non-empty value for any of the given keys in the map.
|
|
||||||
func firstOf(m map[string]string, keys ...string) string {
|
|
||||||
for _, k := range keys {
|
|
||||||
if v := m[k]; v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/alidns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"alidns", "aliyun"},
|
||||||
|
EnvVars: []string{"ALICLOUD_ACCESS_KEY_ID", "ALICLOUD_ACCESS_KEY_SECRET"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
keyID := firstOf(env, "ALICLOUD_ACCESS_KEY_ID", "ALI_ACCESS_KEY_ID", "ALIYUN_ACCESS_KEY_ID")
|
||||||
|
keySecret := firstOf(env, "ALICLOUD_ACCESS_KEY_SECRET", "ALI_ACCESS_KEY_SECRET", "ALIYUN_ACCESS_KEY_SECRET")
|
||||||
|
if keyID == "" || keySecret == "" {
|
||||||
|
return nil, missingEnvErr("alidns", "ALICLOUD_ACCESS_KEY_ID", "ALICLOUD_ACCESS_KEY_SECRET")
|
||||||
|
}
|
||||||
|
return &alidns.Provider{
|
||||||
|
CredentialInfo: alidns.CredentialInfo{
|
||||||
|
AccessKeyID: keyID,
|
||||||
|
AccessKeySecret: keySecret,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/azure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"azure"},
|
||||||
|
EnvVars: []string{
|
||||||
|
"AZURE_SUBSCRIPTION_ID", "AZURE_RESOURCE_GROUP_NAME",
|
||||||
|
"AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET",
|
||||||
|
},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
sub := firstOf(env, "AZURE_SUBSCRIPTION_ID")
|
||||||
|
rg := firstOf(env, "AZURE_RESOURCE_GROUP_NAME", "AZURE_RESOURCE_GROUP")
|
||||||
|
if sub == "" || rg == "" {
|
||||||
|
return nil, missingEnvErr("azure", "AZURE_SUBSCRIPTION_ID", "AZURE_RESOURCE_GROUP_NAME")
|
||||||
|
}
|
||||||
|
// TenantId/ClientId/ClientSecret are optional — when omitted, libdns
|
||||||
|
// falls back to the default Azure credential chain (managed identity,
|
||||||
|
// CLI login, etc.).
|
||||||
|
return &azure.Provider{
|
||||||
|
SubscriptionId: sub,
|
||||||
|
ResourceGroupName: rg,
|
||||||
|
TenantId: firstOf(env, "AZURE_TENANT_ID"),
|
||||||
|
ClientId: firstOf(env, "AZURE_CLIENT_ID"),
|
||||||
|
ClientSecret: firstOf(env, "AZURE_CLIENT_SECRET"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/bunny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"bunny"},
|
||||||
|
EnvVars: []string{"BUNNY_ACCESS_KEY"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
key := firstOf(env, "BUNNY_ACCESS_KEY", "BUNNY_API_KEY")
|
||||||
|
if key == "" {
|
||||||
|
return nil, missingEnvErr("bunny", "BUNNY_ACCESS_KEY")
|
||||||
|
}
|
||||||
|
return &bunny.Provider{AccessKey: key}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"cloudflare", "cf"},
|
||||||
|
EnvVars: []string{"CLOUDFLARE_DNS_API_TOKEN", "CF_API_TOKEN", "CLOUDFLARE_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "CLOUDFLARE_DNS_API_TOKEN", "CF_API_TOKEN", "CLOUDFLARE_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("cloudflare", "CLOUDFLARE_DNS_API_TOKEN", "CF_API_TOKEN")
|
||||||
|
}
|
||||||
|
return &cloudflare.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/desec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"desec"},
|
||||||
|
EnvVars: []string{"DESEC_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "DESEC_TOKEN", "DESEC_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("desec", "DESEC_TOKEN")
|
||||||
|
}
|
||||||
|
return &desec.Provider{Token: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/digitalocean"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"digitalocean", "do"},
|
||||||
|
EnvVars: []string{"DO_AUTH_TOKEN", "DIGITALOCEAN_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "DO_AUTH_TOKEN", "DIGITALOCEAN_TOKEN", "DIGITALOCEAN_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("digitalocean", "DO_AUTH_TOKEN")
|
||||||
|
}
|
||||||
|
return &digitalocean.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/duckdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"duckdns"},
|
||||||
|
EnvVars: []string{"DUCKDNS_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "DUCKDNS_TOKEN", "DUCKDNS_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("duckdns", "DUCKDNS_TOKEN")
|
||||||
|
}
|
||||||
|
return &duckdns.Provider{
|
||||||
|
APIToken: token,
|
||||||
|
OverrideDomain: firstOf(env, "DUCKDNS_OVERRIDE_DOMAIN"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/gandi"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"gandi"},
|
||||||
|
EnvVars: []string{"GANDI_BEARER_TOKEN", "GANDI_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "GANDI_BEARER_TOKEN", "GANDI_API_TOKEN", "GANDI_PERSONAL_ACCESS_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("gandi", "GANDI_BEARER_TOKEN")
|
||||||
|
}
|
||||||
|
return &gandi.Provider{BearerToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/godaddy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"godaddy"},
|
||||||
|
EnvVars: []string{"GODADDY_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
// GoDaddy auth header format: "sso-key KEY:SECRET"
|
||||||
|
// Pass the full "KEY:SECRET" pair via GODADDY_API_TOKEN, OR set
|
||||||
|
// GODADDY_API_KEY + GODADDY_API_SECRET and we'll join them.
|
||||||
|
token := firstOf(env, "GODADDY_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
key := firstOf(env, "GODADDY_API_KEY")
|
||||||
|
secret := firstOf(env, "GODADDY_API_SECRET")
|
||||||
|
if key != "" && secret != "" {
|
||||||
|
token = key + ":" + secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("godaddy", "GODADDY_API_TOKEN", "GODADDY_API_KEY+GODADDY_API_SECRET")
|
||||||
|
}
|
||||||
|
return &godaddy.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/googleclouddns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"googleclouddns", "gcp", "gcloud"},
|
||||||
|
EnvVars: []string{"GCE_PROJECT", "GCE_SERVICE_ACCOUNT_FILE"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
project := firstOf(env, "GCE_PROJECT", "GOOGLE_PROJECT_ID", "GCP_PROJECT")
|
||||||
|
if project == "" {
|
||||||
|
return nil, missingEnvErr("googleclouddns", "GCE_PROJECT")
|
||||||
|
}
|
||||||
|
// ServiceAccountJSON may be a file path OR a raw JSON blob; when
|
||||||
|
// empty, the default Google credential chain is used.
|
||||||
|
return &googleclouddns.Provider{
|
||||||
|
Project: project,
|
||||||
|
ServiceAccountJSON: firstOf(env, "GCE_SERVICE_ACCOUNT_FILE", "GOOGLE_APPLICATION_CREDENTIALS"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// firstOf returns the value of the first non-empty env key from candidates.
|
||||||
|
// Lookup is case-sensitive (env keys are conventionally upper-case).
|
||||||
|
func firstOf(env map[string]string, keys ...string) string {
|
||||||
|
for _, k := range keys {
|
||||||
|
if v := strings.TrimSpace(env[k]); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// missingEnvErr formats a "missing required env" error consistently.
|
||||||
|
func missingEnvErr(provider string, keys ...string) error {
|
||||||
|
return fmt.Errorf("dns provider %q requires one of %s in dns_env", provider, strings.Join(keys, ", "))
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/hetzner"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"hetzner"},
|
||||||
|
EnvVars: []string{"HETZNER_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "HETZNER_API_TOKEN", "HETZNER_DNS_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("hetzner", "HETZNER_API_TOKEN")
|
||||||
|
}
|
||||||
|
return &hetzner.Provider{AuthAPIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/huaweicloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"huaweicloud", "huawei"},
|
||||||
|
EnvVars: []string{"HUAWEICLOUD_ACCESS_KEY_ID", "HUAWEICLOUD_SECRET_ACCESS_KEY", "HUAWEICLOUD_REGION_ID"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
id := firstOf(env, "HUAWEICLOUD_ACCESS_KEY_ID", "HUAWEI_ACCESS_KEY_ID")
|
||||||
|
secret := firstOf(env, "HUAWEICLOUD_SECRET_ACCESS_KEY", "HUAWEI_SECRET_ACCESS_KEY")
|
||||||
|
if id == "" || secret == "" {
|
||||||
|
return nil, missingEnvErr("huaweicloud", "HUAWEICLOUD_ACCESS_KEY_ID", "HUAWEICLOUD_SECRET_ACCESS_KEY")
|
||||||
|
}
|
||||||
|
return &huaweicloud.Provider{
|
||||||
|
AccessKeyId: id,
|
||||||
|
SecretAccessKey: secret,
|
||||||
|
RegionId: firstOf(env, "HUAWEICLOUD_REGION_ID", "HUAWEI_REGION"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/linode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"linode"},
|
||||||
|
EnvVars: []string{"LINODE_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "LINODE_TOKEN", "LINODE_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("linode", "LINODE_TOKEN")
|
||||||
|
}
|
||||||
|
return &linode.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/namecheap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"namecheap"},
|
||||||
|
EnvVars: []string{"NAMECHEAP_API_KEY", "NAMECHEAP_API_USER", "NAMECHEAP_CLIENT_IP"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
apiKey := firstOf(env, "NAMECHEAP_API_KEY")
|
||||||
|
user := firstOf(env, "NAMECHEAP_API_USER", "NAMECHEAP_USER")
|
||||||
|
if apiKey == "" || user == "" {
|
||||||
|
return nil, missingEnvErr("namecheap", "NAMECHEAP_API_KEY", "NAMECHEAP_API_USER")
|
||||||
|
}
|
||||||
|
return &namecheap.Provider{
|
||||||
|
APIKey: apiKey,
|
||||||
|
User: user,
|
||||||
|
APIEndpoint: firstOf(env, "NAMECHEAP_API_ENDPOINT"),
|
||||||
|
ClientIP: firstOf(env, "NAMECHEAP_CLIENT_IP"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/namesilo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"namesilo"},
|
||||||
|
EnvVars: []string{"NAMESILO_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "NAMESILO_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("namesilo", "NAMESILO_API_TOKEN")
|
||||||
|
}
|
||||||
|
return &namesilo.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/netlify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"netlify"},
|
||||||
|
EnvVars: []string{"NETLIFY_PERSONAL_ACCESS_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "NETLIFY_PERSONAL_ACCESS_TOKEN", "NETLIFY_AUTH_TOKEN", "NETLIFY_API_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("netlify", "NETLIFY_PERSONAL_ACCESS_TOKEN")
|
||||||
|
}
|
||||||
|
return &netlify.Provider{PersonalAccessToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/ovh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"ovh"},
|
||||||
|
EnvVars: []string{
|
||||||
|
"OVH_ENDPOINT", "OVH_APPLICATION_KEY", "OVH_APPLICATION_SECRET", "OVH_CONSUMER_KEY",
|
||||||
|
},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
appKey := firstOf(env, "OVH_APPLICATION_KEY")
|
||||||
|
appSecret := firstOf(env, "OVH_APPLICATION_SECRET")
|
||||||
|
consumerKey := firstOf(env, "OVH_CONSUMER_KEY")
|
||||||
|
if appKey == "" || appSecret == "" || consumerKey == "" {
|
||||||
|
return nil, missingEnvErr("ovh", "OVH_APPLICATION_KEY", "OVH_APPLICATION_SECRET", "OVH_CONSUMER_KEY")
|
||||||
|
}
|
||||||
|
return &ovh.Provider{
|
||||||
|
Endpoint: firstOf(env, "OVH_ENDPOINT"),
|
||||||
|
ApplicationKey: appKey,
|
||||||
|
ApplicationSecret: appSecret,
|
||||||
|
ConsumerKey: consumerKey,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/porkbun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"porkbun"},
|
||||||
|
EnvVars: []string{"PORKBUN_API_KEY", "PORKBUN_API_SECRET_KEY"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
key := firstOf(env, "PORKBUN_API_KEY")
|
||||||
|
secret := firstOf(env, "PORKBUN_API_SECRET_KEY", "PORKBUN_SECRET_API_KEY")
|
||||||
|
if key == "" || secret == "" {
|
||||||
|
return nil, missingEnvErr("porkbun", "PORKBUN_API_KEY", "PORKBUN_API_SECRET_KEY")
|
||||||
|
}
|
||||||
|
return &porkbun.Provider{APIKey: key, APISecretKey: secret}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// Package dnsproviders is a registry of DNS providers usable for ACME DNS-01
|
||||||
|
// challenges. Each provider is implemented as a separate file that registers
|
||||||
|
// itself in init(); to add a new provider, drop a file in this package — no
|
||||||
|
// changes are needed elsewhere.
|
||||||
|
//
|
||||||
|
// All providers wrap the libdns ecosystem (https://github.com/libdns) so they
|
||||||
|
// plug directly into certmagic's DNSManager.
|
||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder constructs a certmagic.DNSProvider from a flat env-style map.
|
||||||
|
// The map mirrors the cert_config.dns_env block from config.yml.
|
||||||
|
type Builder func(env map[string]string) (certmagic.DNSProvider, error)
|
||||||
|
|
||||||
|
// Provider describes a single DNS provider entry in the registry.
|
||||||
|
type Provider struct {
|
||||||
|
// Names is the list of accepted dns_provider values; the first is canonical,
|
||||||
|
// the rest are aliases (case-insensitive match).
|
||||||
|
Names []string
|
||||||
|
// EnvVars lists the environment variable names this provider may consume.
|
||||||
|
// Used for documentation and error messages only.
|
||||||
|
EnvVars []string
|
||||||
|
// Build constructs the underlying libdns provider.
|
||||||
|
Build Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mu sync.RWMutex
|
||||||
|
registry = map[string]*Provider{}
|
||||||
|
ordered []*Provider
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register adds a provider to the global registry. It panics on duplicate
|
||||||
|
// names so that registration errors surface at process start.
|
||||||
|
func Register(p *Provider) {
|
||||||
|
if p == nil || len(p.Names) == 0 || p.Build == nil {
|
||||||
|
panic("dnsproviders: invalid provider registration")
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
for _, n := range p.Names {
|
||||||
|
key := strings.ToLower(strings.TrimSpace(n))
|
||||||
|
if key == "" {
|
||||||
|
panic("dnsproviders: empty provider name")
|
||||||
|
}
|
||||||
|
if _, exists := registry[key]; exists {
|
||||||
|
panic(fmt.Sprintf("dnsproviders: duplicate name %q", key))
|
||||||
|
}
|
||||||
|
registry[key] = p
|
||||||
|
}
|
||||||
|
ordered = append(ordered, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the provider registered under the given (case-insensitive) name
|
||||||
|
// or alias.
|
||||||
|
func Get(name string) (*Provider, bool) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
p, ok := registry[strings.ToLower(strings.TrimSpace(name))]
|
||||||
|
return p, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalNames returns the canonical (first-listed) name of every registered
|
||||||
|
// provider, sorted alphabetically. Useful for error messages.
|
||||||
|
func CanonicalNames() []string {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
names := make([]string, 0, len(ordered))
|
||||||
|
for _, p := range ordered {
|
||||||
|
names = append(names, p.Names[0])
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all registered providers (canonical entries only), sorted by
|
||||||
|
// canonical name.
|
||||||
|
func List() []*Provider {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
out := make([]*Provider, len(ordered))
|
||||||
|
copy(out, ordered)
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Names[0] < out[j].Names[0] })
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRegistryHasMainstreamProviders verifies that all expected mainstream
|
||||||
|
// providers are registered with their canonical names (and key aliases).
|
||||||
|
func TestRegistryHasMainstreamProviders(t *testing.T) {
|
||||||
|
cases := map[string][]string{
|
||||||
|
"cloudflare": {"cf"},
|
||||||
|
"alidns": {"aliyun"},
|
||||||
|
"tencentcloud": {"tencent"},
|
||||||
|
"route53": {"aws"},
|
||||||
|
"godaddy": nil,
|
||||||
|
"namecheap": nil,
|
||||||
|
"namesilo": nil,
|
||||||
|
"digitalocean": {"do"},
|
||||||
|
"linode": nil,
|
||||||
|
"hetzner": nil,
|
||||||
|
"gandi": nil,
|
||||||
|
"porkbun": nil,
|
||||||
|
"netlify": nil,
|
||||||
|
"azure": nil,
|
||||||
|
"googleclouddns": {"gcp", "gcloud"},
|
||||||
|
"huaweicloud": {"huawei"},
|
||||||
|
"bunny": nil,
|
||||||
|
"duckdns": nil,
|
||||||
|
"ovh": nil,
|
||||||
|
"vultr": nil,
|
||||||
|
"desec": nil,
|
||||||
|
}
|
||||||
|
for canonical, aliases := range cases {
|
||||||
|
if _, ok := Get(canonical); !ok {
|
||||||
|
t.Errorf("provider %q not registered", canonical)
|
||||||
|
}
|
||||||
|
// case-insensitive lookup must work
|
||||||
|
if _, ok := Get(strings.ToUpper(canonical)); !ok {
|
||||||
|
t.Errorf("provider %q not found case-insensitively", canonical)
|
||||||
|
}
|
||||||
|
for _, a := range aliases {
|
||||||
|
if _, ok := Get(a); !ok {
|
||||||
|
t.Errorf("alias %q for %q not registered", a, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryRejectsUnknown(t *testing.T) {
|
||||||
|
if _, ok := Get("definitely-not-a-real-provider"); ok {
|
||||||
|
t.Fatal("Get returned ok for unknown provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProvidersValidateMissingEnv ensures every provider's Build returns a
|
||||||
|
// helpful error when no credentials are supplied (rather than panicking or
|
||||||
|
// silently building an unusable client).
|
||||||
|
//
|
||||||
|
// route53/azure/googleclouddns are excluded because they intentionally fall
|
||||||
|
// back to an ambient credential chain (IAM role, managed identity, ADC) and
|
||||||
|
// produce a usable provider even with empty config.
|
||||||
|
func TestProvidersValidateMissingEnv(t *testing.T) {
|
||||||
|
excluded := map[string]bool{
|
||||||
|
"route53": true,
|
||||||
|
"azure": true, // partially excluded — actually still requires sub+rg
|
||||||
|
"googleclouddns": true, // requires GCE_PROJECT
|
||||||
|
}
|
||||||
|
for _, p := range List() {
|
||||||
|
canonical := p.Names[0]
|
||||||
|
if excluded[canonical] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Run(canonical, func(t *testing.T) {
|
||||||
|
_, err := p.Build(map[string]string{})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("provider %q built successfully with empty env; expected missing-credential error", canonical)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanonicalNamesSorted(t *testing.T) {
|
||||||
|
names := CanonicalNames()
|
||||||
|
for i := 1; i < len(names); i++ {
|
||||||
|
if names[i-1] > names[i] {
|
||||||
|
t.Fatalf("CanonicalNames not sorted: %v", names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/route53"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"route53", "aws"},
|
||||||
|
EnvVars: []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_SESSION_TOKEN", "AWS_HOSTED_ZONE_ID", "AWS_PROFILE"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
// route53 supports the full AWS credential chain (env, IAM role, profile,
|
||||||
|
// shared credentials file). We deliberately do not require any field —
|
||||||
|
// users running on EC2/EKS may rely entirely on the instance role.
|
||||||
|
return &route53.Provider{
|
||||||
|
AccessKeyId: firstOf(env, "AWS_ACCESS_KEY_ID"),
|
||||||
|
SecretAccessKey: firstOf(env, "AWS_SECRET_ACCESS_KEY"),
|
||||||
|
SessionToken: firstOf(env, "AWS_SESSION_TOKEN"),
|
||||||
|
Region: firstOf(env, "AWS_REGION", "AWS_DEFAULT_REGION"),
|
||||||
|
Profile: firstOf(env, "AWS_PROFILE"),
|
||||||
|
HostedZoneID: firstOf(env, "AWS_HOSTED_ZONE_ID"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/tencentcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"tencentcloud", "tencent"},
|
||||||
|
EnvVars: []string{"TENCENTCLOUD_SECRET_ID", "TENCENTCLOUD_SECRET_KEY", "TENCENTCLOUD_REGION"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
id := firstOf(env, "TENCENTCLOUD_SECRET_ID", "TENCENT_SECRET_ID")
|
||||||
|
key := firstOf(env, "TENCENTCLOUD_SECRET_KEY", "TENCENT_SECRET_KEY")
|
||||||
|
if id == "" || key == "" {
|
||||||
|
return nil, missingEnvErr("tencentcloud", "TENCENTCLOUD_SECRET_ID", "TENCENTCLOUD_SECRET_KEY")
|
||||||
|
}
|
||||||
|
return &tencentcloud.Provider{
|
||||||
|
SecretId: id,
|
||||||
|
SecretKey: key,
|
||||||
|
Region: firstOf(env, "TENCENTCLOUD_REGION"),
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dnsproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/vultr/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Provider{
|
||||||
|
Names: []string{"vultr"},
|
||||||
|
EnvVars: []string{"VULTR_API_TOKEN"},
|
||||||
|
Build: func(env map[string]string) (certmagic.DNSProvider, error) {
|
||||||
|
token := firstOf(env, "VULTR_API_TOKEN", "VULTR_API_KEY")
|
||||||
|
if token == "" {
|
||||||
|
return nil, missingEnvErr("vultr", "VULTR_API_TOKEN")
|
||||||
|
}
|
||||||
|
return &vultr.Provider{APIToken: token}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cedar2025/xboard-node/internal/cert"
|
"github.com/cedar2025/xboard-node/internal/cert"
|
||||||
|
"github.com/cedar2025/xboard-node/internal/cert/dnsproviders"
|
||||||
"github.com/cedar2025/xboard-node/internal/config"
|
"github.com/cedar2025/xboard-node/internal/config"
|
||||||
"github.com/cedar2025/xboard-node/internal/controlplane"
|
"github.com/cedar2025/xboard-node/internal/controlplane"
|
||||||
"github.com/cedar2025/xboard-node/internal/kernel"
|
"github.com/cedar2025/xboard-node/internal/kernel"
|
||||||
@@ -1219,15 +1220,14 @@ func validateRuntimeCertConfig(spec *model.NodeSpec) error {
|
|||||||
if mode != "dns" {
|
if mode != "dns" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
provider := strings.ToLower(strings.TrimSpace(spec.CertConfig.DNSProvider))
|
provider := strings.TrimSpace(spec.CertConfig.DNSProvider)
|
||||||
switch provider {
|
if provider == "" {
|
||||||
case "cloudflare", "cf", "alidns", "aliyun":
|
|
||||||
return nil
|
|
||||||
case "":
|
|
||||||
return fmt.Errorf("dns cert mode requires cert_config.dns_provider")
|
return fmt.Errorf("dns cert mode requires cert_config.dns_provider")
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported cert_config.dns_provider %q (supported: cloudflare, alidns)", spec.CertConfig.DNSProvider)
|
|
||||||
}
|
}
|
||||||
|
if _, ok := dnsproviders.Get(provider); !ok {
|
||||||
|
return fmt.Errorf("unsupported cert_config.dns_provider %q (supported: %s)", provider, strings.Join(dnsproviders.CanonicalNames(), ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRealityRequirements(spec *model.NodeSpec, _ string) error {
|
func validateRealityRequirements(spec *model.NodeSpec, _ string) error {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/cedar2025/xboard-node/internal/cert"
|
"github.com/cedar2025/xboard-node/internal/cert"
|
||||||
@@ -214,7 +215,7 @@ func TestValidateNodeRuntimeRejectsUnsupportedDNSProvider(t *testing.T) {
|
|||||||
if err.Error() == "" {
|
if err.Error() == "" {
|
||||||
t.Fatal("expected non-empty error")
|
t.Fatal("expected non-empty error")
|
||||||
}
|
}
|
||||||
if got := err.Error(); got != "unsupported cert_config.dns_provider \"3123123\" (supported: cloudflare, alidns)" {
|
if got := err.Error(); !strings.HasPrefix(got, `unsupported cert_config.dns_provider "3123123" (supported: `) {
|
||||||
t.Fatalf("unexpected error: %v", got)
|
t.Fatalf("unexpected error: %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user