feat(provider): new acme dns-01 provider: rfc2136

This commit is contained in:
Fu Diwei 2025-10-22 22:25:07 +08:00
parent 1f6ef6dcb3
commit c2a2ad6cb0
13 changed files with 242 additions and 12 deletions

View File

@ -0,0 +1,33 @@
package applicators
import (
"fmt"
"github.com/go-acme/lego/v4/challenge"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/rfc2136"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeRFC2136, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
credentials := domain.AccessConfigForRFC2136{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := rfc2136.NewChallengeProvider(&rfc2136.ChallengeProviderConfig{
Host: credentials.Host,
Port: credentials.Port,
TsigAlgorithm: credentials.TsigAlgorithm,
TsigKey: credentials.TsigKey,
TsigSecret: credentials.TsigSecret,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return provider, err
}); err != nil {
panic(err)
}
}

View File

@ -202,6 +202,15 @@ type AccessConfigForGcore struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForGlobalSectigo struct {
AccessConfigForACMEExternalAccountBinding
ValidationType string `json:"validationType"`
}
type AccessConfigForGlobalSignAtlas struct {
AccessConfigForACMEExternalAccountBinding
}
type AccessConfigForGname struct {
AppId string `json:"appId"`
AppKey string `json:"appKey"`
@ -220,10 +229,6 @@ type AccessConfigForGoEdge struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForGlobalSignAtlas struct {
AccessConfigForACMEExternalAccountBinding
}
type AccessConfigForGoogleTrustServices struct {
AccessConfigForACMEExternalAccountBinding
}
@ -335,17 +340,20 @@ type AccessConfigForRatPanel struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForRFC2136 struct {
Host string `json:"host"`
Port int32 `json:"port"`
TsigAlgorithm string `json:"tsigAlgorithm,omitempty"`
TsigKey string `json:"tsigKey,omitempty"`
TsigSecret string `json:"tsigSecret,omitempty"`
}
type AccessConfigForSafeLine struct {
ServerUrl string `json:"serverUrl"`
ApiToken string `json:"apiToken"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForGlobalSectigo struct {
AccessConfigForACMEExternalAccountBinding
ValidationType string `json:"validationType"`
}
type AccessConfigForSlackBot struct {
BotToken string `json:"botToken"`
ChannelId string `json:"channelId,omitempty"`

View File

@ -79,7 +79,7 @@ const (
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
AccessProviderTypeRFC2136 = AccessProviderType("rfc2136") // RFC2136预留
AccessProviderTypeRFC2136 = AccessProviderType("rfc2136")
AccessProviderTypeSafeLine = AccessProviderType("safeline")
AccessProviderTypeSectigo = AccessProviderType("sectigo")
AccessProviderTypeSlackBot = AccessProviderType("slackbot")
@ -174,6 +174,7 @@ const (
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136)
ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship)
ACMEDns01ProviderTypeTechnitiumDNS = ACMEDns01ProviderType(AccessProviderTypeTechnitiumDNS)
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]

View File

@ -0,0 +1,55 @@
package rfc2136
import (
"errors"
"net"
"strconv"
"time"
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
"github.com/certimate-go/certimate/pkg/core"
)
type ChallengeProviderConfig struct {
Host string `json:"host"`
Port int32 `json:"port"`
TsigAlgorithm string `json:"tsigAlgorithm,omitempty"`
TsigKey string `json:"tsigKey,omitempty"`
TsigSecret string `json:"tsigSecret,omitempty"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
if config == nil {
return nil, errors.New("the configuration of the acme challenge provider is nil")
}
if config.Port == 0 {
config.Port = 53
}
if config.TsigAlgorithm == "" {
config.TsigAlgorithm = "hmac-sha1."
}
providerConfig := rfc2136.NewDefaultConfig()
providerConfig.Nameserver = net.JoinHostPort(config.Host, strconv.Itoa(int(config.Port)))
providerConfig.TSIGAlgorithm = config.TsigAlgorithm
providerConfig.TSIGKey = config.TsigKey
providerConfig.TSIGSecret = config.TsigSecret
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := rfc2136.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

View File

@ -70,6 +70,7 @@ import AccessConfigFieldsProviderProxmoxVE from "./forms/AccessConfigFieldsProvi
import AccessConfigFieldsProviderQiniu from "./forms/AccessConfigFieldsProviderQiniu";
import AccessConfigFieldsProviderRainYun from "./forms/AccessConfigFieldsProviderRainYun";
import AccessConfigFieldsProviderRatPanel from "./forms/AccessConfigFieldsProviderRatPanel";
import AccessConfigFieldsProviderRFC2136 from "./forms/AccessConfigFieldsProviderRFC2136";
import AccessConfigFieldsProviderSafeLine from "./forms/AccessConfigFieldsProviderSafeLine";
import AccessConfigFieldsProviderSectigo from "./forms/AccessConfigFieldsProviderSectigo";
import AccessConfigFieldsProviderSlackBot from "./forms/AccessConfigFieldsProviderSlackBot";
@ -311,6 +312,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, on
case ACCESS_PROVIDERS.RATPANEL: {
return <AccessConfigFieldsProviderRatPanel />;
}
case ACCESS_PROVIDERS.RFC2136: {
return <AccessConfigFieldsProviderRFC2136 />;
}
case ACCESS_PROVIDERS.SAFELINE: {
return <AccessConfigFieldsProviderSafeLine />;
}

View File

@ -36,8 +36,8 @@ const AccessConfigFormFieldsProviderACMEHttpReq = () => {
>
<Select
options={[
{ value: "", label: "(default)" },
{ value: "RAW", label: "RAW" },
{ label: "(default)", value: "" },
{ label: "RAW", value: "RAW" },
]}
placeholder={t("access.form.acmehttpreq_mode.placeholder")}
/>

View File

@ -0,0 +1,103 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select, InputNumber } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderRFC2136 = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<div className="flex space-x-2">
<div className="w-2/3">
<Form.Item name={[parentNamePath, "host"]} initialValue={initialValues.host} label={t("access.form.rfc2136_host.label")} rules={[formRule]}>
<Input placeholder={t("access.form.rfc2136_host.placeholder")} />
</Form.Item>
</div>
<div className="w-1/3">
<Form.Item name={[parentNamePath, "port"]} initialValue={initialValues.port} label={t("access.form.rfc2136_port.label")} rules={[formRule]}>
<InputNumber style={{ width: "100%" }} min={1} max={65535} placeholder={t("access.form.rfc2136_port.placeholder")} />
</Form.Item>
</div>
</div>
<Form.Item
name={[parentNamePath, "tsigAlgorithm"]}
initialValue={initialValues.tsigAlgorithm}
label={t("access.form.rfc2136_tsig_algorithm.label")}
rules={[formRule]}
>
<Select
options={[
{ label: "HMAC-SHA-1", value: "hmac-sha1." },
{ label: "HMAC-SHA-224", value: "hmac-sha224." },
{ label: "HMAC-SHA-256", value: "hmac-sha256." },
{ label: "HMAC-SHA-384", value: "hmac-sha384." },
{ label: "HMAC-SHA-512", value: "hmac-sha512." },
]}
placeholder={t("access.form.rfc2136_tsig_algorithm.placeholder")}
/>
</Form.Item>
<Form.Item name={[parentNamePath, "tsigKey"]} initialValue={initialValues.tsigKey} label={t("access.form.rfc2136_tsig_key.label")} rules={[formRule]}>
<Input allowClear autoComplete="new-password" placeholder={t("access.form.rfc2136_tsig_key.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "tsigSecret"]}
initialValue={initialValues.tsigSecret}
label={t("access.form.rfc2136_tsig_secret.label")}
rules={[formRule]}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.rfc2136_tsig_secret.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
host: "127.0.0.1",
port: 53,
tsigAlgorithm: "hmac-sha1.",
tsigKey: "",
tsigSecret: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
host: z.string().refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
port: z.preprocess(
(v) => Number(v),
z
.number()
.int(t("access.form.rfc2136_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
tsigAlgorithm: z.string().nonempty(t("access.form.rfc2136_tsig_algorithm.placeholder")),
tsigKey: z.string().nullish(),
tsigSecret: z.string().nullish(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderRFC2136, {
getInitialValues,
getSchema,
});
export default _default;

View File

@ -77,6 +77,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
QINIU: "qiniu",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
RFC2136: "rfc2136",
SAFELINE: "safeline",
SECTIGO: "sectigo",
SLACKBOT: "slackbot",
@ -187,6 +188,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.WESTCN, "provider.westcn", "/imgs/providers/westcn.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.POWERDNS, "provider.powerdns", "/imgs/providers/powerdns.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.TECHNITIUMDNS, "provider.technitiumdns", "/imgs/providers/technitiumdns.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.RFC2136, "provider.rfc2136", "/imgs/providers/rfc.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.ACMEDNS, "provider.acmedns", "/imgs/providers/acmedns.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq", "/imgs/providers/acmehttpreq.svg", [ACCESS_USAGES.DNS]],
@ -320,6 +322,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
PORKBUN: `${ACCESS_PROVIDERS.PORKBUN}`,
POWERDNS: `${ACCESS_PROVIDERS.POWERDNS}`,
RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,
RFC2136: `${ACCESS_PROVIDERS.RFC2136}`,
SPACESHIP: `${ACCESS_PROVIDERS.SPACESHIP}`,
UCLOUD_UDNR: `${ACCESS_PROVIDERS.UCLOUD}-udnr`,
TECHNITIUMDNS: `${ACCESS_PROVIDERS.TECHNITIUMDNS}`,
@ -384,6 +387,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.WESTCN, "provider.westcn"],
[ACME_DNS01_PROVIDERS.POWERDNS, "provider.powerdns"],
[ACME_DNS01_PROVIDERS.TECHNITIUMDNS, "provider.technitiumdns"],
[ACME_DNS01_PROVIDERS.RFC2136, "provider.rfc2136"],
[ACME_DNS01_PROVIDERS.ACMEDNS, "provider.acmedns"],
[ACME_DNS01_PROVIDERS.ACMEHTTPREQ, "provider.acmehttpreq"],
] satisfies Array<[ACMEDns01ProviderType, string]>

View File

@ -405,6 +405,16 @@
"access.form.ratpanel_access_token.label": "RatPanel access token",
"access.form.ratpanel_access_token.placeholder": "Please enter RatPanel access token",
"access.form.ratpanel_access_token.tooltip": "For more information, see <a href=\"https://ratpanel.github.io/advanced/api.html\" target=\"_blank\">https://ratpanel.github.io/advanced/api.html</a>",
"access.form.rfc2136_host.label": "DNS server host",
"access.form.rfc2136_host.placeholder": "Please enter DNS server host",
"access.form.rfc2136_port.label": "DNS server port",
"access.form.rfc2136_port.placeholder": "Please enter DNS server port",
"access.form.rfc2136_tsig_algorithm.label": "TSIG algorithm",
"access.form.rfc2136_tsig_algorithm.placeholder": "Please select TSIG algorithm",
"access.form.rfc2136_tsig_key.label": "TSIG authentication key (Optional)",
"access.form.rfc2136_tsig_key.placeholder": "Please enter TSIG authentication key",
"access.form.rfc2136_tsig_secret.label": "TSIG authentication secret (Optional)",
"access.form.rfc2136_tsig_secret.placeholder": "Please enter TSIG authentication secret",
"access.form.safeline_server_url.label": "SafeLine server URL",
"access.form.safeline_server_url.placeholder": "Please enter SafeLine server URL",
"access.form.safeline_api_token.label": "SafeLine API token",

View File

@ -132,6 +132,7 @@
"provider.ratpanel": "RatPanel",
"provider.ratpanel.console": "RatPanel - Console itself",
"provider.ratpanel.site": "RatPanel - Website",
"provider.rfc2136": "RFC 2136: Dynamic DNS Updates",
"provider.safeline": "SafeLine",
"provider.sectigo": "Sectigo",
"provider.slackbot": "Slack Bot",

View File

@ -404,6 +404,16 @@
"access.form.ratpanel_access_token.label": "耗子面板 AccessToken",
"access.form.ratpanel_access_token.placeholder": "请输入耗子面板 AccessToken",
"access.form.ratpanel_access_token.tooltip": "这是什么?请参阅 <a href=\"https://ratpanel.github.io/advanced/api.html\" target=\"_blank\">https://ratpanel.github.io/advanced/api.html</a>",
"access.form.rfc2136_host.label": "DNS 服务器地址",
"access.form.rfc2136_host.placeholder": "请输入 DNS 服务器地址",
"access.form.rfc2136_port.label": "DNS 服务器端口",
"access.form.rfc2136_port.placeholder": "请输入 DNS 服务器端口",
"access.form.rfc2136_tsig_algorithm.label": "TSIG 算法",
"access.form.rfc2136_tsig_algorithm.placeholder": "请选择 TSIG 算法",
"access.form.rfc2136_tsig_key.label": "TSIG 认证密钥 Key可选",
"access.form.rfc2136_tsig_key.placeholder": "请输入 TSIG 认证密钥 Key",
"access.form.rfc2136_tsig_secret.label": "TSIG 认证密钥 Secret可选",
"access.form.rfc2136_tsig_secret.placeholder": "请输入 TSIG 认证密钥 Secret",
"access.form.safeline_server_url.label": "雷池服务地址",
"access.form.safeline_server_url.placeholder": "请输入雷池服务地址",
"access.form.safeline_api_token.label": "雷池 API Token",

View File

@ -132,6 +132,7 @@
"provider.ratpanel": "耗子面板",
"provider.ratpanel.console": "耗子面板 - 面板自身",
"provider.ratpanel.site": "耗子面板 - 网站",
"provider.rfc2136": "RFC 2136: Dynamic DNS Updates",
"provider.safeline": "雷池",
"provider.sectigo": "Sectigo",
"provider.slackbot": "Slack 机器人",