添加证书自动签发

This commit is contained in:
CN-JS-HuiBai
2026-04-15 18:42:45 +08:00
parent dfdf9a8ed7
commit abc7c0d933
3 changed files with 209 additions and 5 deletions

View File

@@ -89,7 +89,7 @@ build_sing_box() {
# Build params from Makefile # Build params from Makefile
VERSION=$(git rev-parse --short HEAD 2>/dev/null || echo "custom") VERSION=$(git rev-parse --short HEAD 2>/dev/null || echo "custom")
# Reduced tags for safer build on smaller servers # Reduced tags for safer build on smaller servers
TAGS="with_quic,with_utls,with_clash_api,with_gvisor" TAGS="with_quic,with_utls,with_clash_api,with_gvisor,with_acme"
echo -e "${YELLOW}Starting compilation (this may take a few minutes)...${NC}" echo -e "${YELLOW}Starting compilation (this may take a few minutes)...${NC}"
echo -e "${YELLOW}Note: Using -p 1 to save memory and avoid silent crashes.${NC}" echo -e "${YELLOW}Note: Using -p 1 to save memory and avoid silent crashes.${NC}"

View File

@@ -184,6 +184,9 @@ type XInnerConfig struct {
StreamSettings json.RawMessage `json:"streamSettings"` StreamSettings json.RawMessage `json:"streamSettings"`
UpMbps int `json:"up_mbps"` UpMbps int `json:"up_mbps"`
DownMbps int `json:"down_mbps"` DownMbps int `json:"down_mbps"`
CertConfig *XCertConfig `json:"cert_config,omitempty"`
AutoTLS bool `json:"auto_tls,omitempty"`
Domain string `json:"domain,omitempty"`
} }
type XMultiplexConfig struct { type XMultiplexConfig struct {
@@ -438,6 +441,94 @@ func applyCertConfig(tlsOptions *option.InboundTLSOptions, certConfig *XCertConf
} }
} }
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func mergedCertConfig(inner XInnerConfig, config *XNodeConfig) *XCertConfig {
if inner.CertConfig != nil {
return inner.CertConfig
}
if config != nil {
return config.CertConfig
}
return nil
}
func hasUsableServerTLS(tlsOptions option.InboundTLSOptions) bool {
return tlsOptions.CertificateProvider != nil ||
tlsOptions.ACME != nil && len(tlsOptions.ACME.Domain) > 0 ||
(tlsOptions.CertificatePath != "" && tlsOptions.KeyPath != "") ||
(len(tlsOptions.Certificate) > 0 && len(tlsOptions.Key) > 0)
}
func applyACMEConfig(tlsOptions *option.InboundTLSOptions, certConfig *XCertConfig, autoTLS bool, domain string, listenPort int) bool {
if !autoTLS && certConfig == nil {
return false
}
mode := ""
if certConfig != nil {
mode = strings.ToLower(strings.TrimSpace(certConfig.CertMode))
}
if mode == "" && autoTLS {
mode = "http"
}
if mode != "http" && mode != "dns" {
return false
}
domain = strings.TrimSpace(domain)
if domain == "" {
return false
}
acmeOptions := &option.InboundACMEOptions{
Domain: badoption.Listable[string]{domain},
DataDirectory: "acme",
DefaultServerName: domain,
DisableHTTPChallenge: true,
}
if certConfig != nil {
acmeOptions.Email = strings.TrimSpace(certConfig.Email)
}
if listenPort > 0 && listenPort != 443 {
acmeOptions.AlternativeTLSPort = uint16(listenPort)
}
if mode == "dns" && certConfig != nil {
dnsProvider := strings.ToLower(strings.TrimSpace(certConfig.DNSProvider))
if dnsProvider == "" {
return false
}
acmeOptions.DisableHTTPChallenge = true
acmeOptions.DisableTLSALPNChallenge = true
dns01 := &option.ACMEDNS01ChallengeOptions{Provider: dnsProvider}
switch dnsProvider {
case C.DNSProviderCloudflare:
dns01.CloudflareOptions.APIToken = firstNonEmpty(certConfig.DNSEnv["CF_API_TOKEN"], certConfig.DNSEnv["CLOUDFLARE_API_TOKEN"])
dns01.CloudflareOptions.ZoneToken = firstNonEmpty(certConfig.DNSEnv["CF_ZONE_TOKEN"], certConfig.DNSEnv["CLOUDFLARE_ZONE_TOKEN"])
case C.DNSProviderAliDNS:
dns01.AliDNSOptions.AccessKeyID = firstNonEmpty(certConfig.DNSEnv["ALICLOUD_ACCESS_KEY_ID"], certConfig.DNSEnv["ALI_ACCESS_KEY_ID"])
dns01.AliDNSOptions.AccessKeySecret = firstNonEmpty(certConfig.DNSEnv["ALICLOUD_ACCESS_KEY_SECRET"], certConfig.DNSEnv["ALI_ACCESS_KEY_SECRET"])
dns01.AliDNSOptions.RegionID = firstNonEmpty(certConfig.DNSEnv["ALICLOUD_REGION_ID"], certConfig.DNSEnv["ALI_REGION_ID"])
dns01.AliDNSOptions.SecurityToken = firstNonEmpty(certConfig.DNSEnv["ALICLOUD_SECURITY_TOKEN"], certConfig.DNSEnv["ALI_SECURITY_TOKEN"])
case C.DNSProviderACMEDNS:
dns01.ACMEDNSOptions.Username = certConfig.DNSEnv["ACMEDNS_USERNAME"]
dns01.ACMEDNSOptions.Password = certConfig.DNSEnv["ACMEDNS_PASSWORD"]
dns01.ACMEDNSOptions.Subdomain = certConfig.DNSEnv["ACMEDNS_SUBDOMAIN"]
dns01.ACMEDNSOptions.ServerURL = certConfig.DNSEnv["ACMEDNS_SERVER_URL"]
default:
return false
}
acmeOptions.DNS01Challenge = dns01
}
tlsOptions.ACME = acmeOptions
return true
}
func mergedTLSSettings(inner XInnerConfig, config *XNodeConfig) *XTLSSettings { func mergedTLSSettings(inner XInnerConfig, config *XNodeConfig) *XTLSSettings {
tlsSettings := inner.TLSSettings tlsSettings := inner.TLSSettings
if tlsSettings == nil { if tlsSettings == nil {
@@ -880,6 +971,15 @@ func (s *Service) setupNode() error {
if len(inner.NetworkSettings_) == 0 { if len(inner.NetworkSettings_) == 0 {
inner.NetworkSettings_ = config.NetworkSettings_ inner.NetworkSettings_ = config.NetworkSettings_
} }
if inner.CertConfig == nil {
inner.CertConfig = config.CertConfig
}
if !inner.AutoTLS {
inner.AutoTLS = config.AutoTLS
}
if inner.Domain == "" {
inner.Domain = config.Domain
}
// Resolve protocol // Resolve protocol
protocol := inner.Protocol protocol := inner.Protocol
@@ -950,9 +1050,31 @@ func (s *Service) setupNode() error {
if tlsSettings != nil && tlsSettings.ServerName != "" { if tlsSettings != nil && tlsSettings.ServerName != "" {
s.vlessServerName = tlsSettings.ServerName s.vlessServerName = tlsSettings.ServerName
} }
hasCertificate := applyCertConfig(&tlsOptions, config.CertConfig) certConfig := mergedCertConfig(inner, config)
if config.CertConfig != nil && !hasCertificate && config.CertConfig.CertMode != "" && config.CertConfig.CertMode != "none" { hasCertificate := applyCertConfig(&tlsOptions, certConfig)
s.logger.Warn("Xboard cert_config present but unsupported or incomplete for local TLS. cert_mode=", config.CertConfig.CertMode) configDomain := ""
configAutoTLS := false
if config != nil {
configDomain = config.Domain
configAutoTLS = config.AutoTLS
}
certDomain := ""
if certConfig != nil {
certDomain = certConfig.Domain
}
autoTLSDomain := firstNonEmpty(inner.Domain, configDomain, certDomain)
if autoTLSDomain == "" && tlsSettings != nil {
autoTLSDomain = tlsSettings.ServerName
}
hasACME := false
if !hasCertificate {
hasACME = applyACMEConfig(&tlsOptions, certConfig, inner.AutoTLS || configAutoTLS, autoTLSDomain, inner.Port)
}
if certConfig != nil && !hasCertificate && !hasACME && certConfig.CertMode != "" && certConfig.CertMode != "none" {
s.logger.Warn("Xboard cert_config present but unsupported or incomplete for local TLS. cert_mode=", certConfig.CertMode)
}
if hasACME {
s.logger.Info("Xboard ACME configured for domain ", autoTLSDomain)
} }
switch securityType { switch securityType {
@@ -1043,7 +1165,7 @@ func (s *Service) setupNode() error {
} }
} }
if securityType == 1 && !tlsOptions.Enabled { if securityType == 1 && !tlsOptions.Enabled && !hasUsableServerTLS(tlsOptions) {
s.logger.Warn("Xboard TLS enabled by panel but no usable certificate material found; inbound will run without local TLS") s.logger.Warn("Xboard TLS enabled by panel but no usable certificate material found; inbound will run without local TLS")
} }
@@ -1163,6 +1285,9 @@ func (s *Service) setupNode() error {
inboundOptions = ssOptions inboundOptions = ssOptions
case "trojan": case "trojan":
if !hasUsableServerTLS(tlsOptions) {
return fmt.Errorf("trojan requires local TLS certificate or ACME; configure cert_mode file/content/http/dns or auto_tls with domain")
}
// Trojan supports ws/grpc transport like V2bX // Trojan supports ws/grpc transport like V2bX
transport, err := getInboundTransport(networkType, networkSettings) transport, err := getInboundTransport(networkType, networkSettings)
if err != nil { if err != nil {
@@ -1181,6 +1306,9 @@ func (s *Service) setupNode() error {
} }
inboundOptions = opts inboundOptions = opts
case "tuic": case "tuic":
if !hasUsableServerTLS(tlsOptions) {
return fmt.Errorf("tuic requires local TLS certificate or ACME; configure cert_mode file/content/http/dns or auto_tls with domain")
}
// V2bX: TUIC always uses TLS with h3 ALPN // V2bX: TUIC always uses TLS with h3 ALPN
tuicTLS := tlsOptions tuicTLS := tlsOptions
tuicTLS.Enabled = true tuicTLS.Enabled = true
@@ -1202,6 +1330,9 @@ func (s *Service) setupNode() error {
inboundOptions = opts inboundOptions = opts
s.logger.Info("Xboard TUIC configured. CongestionControl: ", congestionControl) s.logger.Info("Xboard TUIC configured. CongestionControl: ", congestionControl)
case "hysteria": case "hysteria":
if !hasUsableServerTLS(tlsOptions) {
return fmt.Errorf("hysteria requires local TLS certificate or ACME; configure cert_mode file/content/http/dns or auto_tls with domain")
}
// V2bX: Hysteria always uses TLS // V2bX: Hysteria always uses TLS
hyTLS := tlsOptions hyTLS := tlsOptions
hyTLS.Enabled = true hyTLS.Enabled = true
@@ -1218,6 +1349,9 @@ func (s *Service) setupNode() error {
inboundOptions = opts inboundOptions = opts
s.logger.Info("Xboard Hysteria configured. Up: ", config.UpMbps, " Down: ", config.DownMbps) s.logger.Info("Xboard Hysteria configured. Up: ", config.UpMbps, " Down: ", config.DownMbps)
case "hysteria2": case "hysteria2":
if !hasUsableServerTLS(tlsOptions) {
return fmt.Errorf("hysteria2 requires local TLS certificate or ACME; configure cert_mode file/content/http/dns or auto_tls with domain")
}
// V2bX: Hysteria2 always uses TLS, optional obfs // V2bX: Hysteria2 always uses TLS, optional obfs
hy2TLS := tlsOptions hy2TLS := tlsOptions
hy2TLS.Enabled = true hy2TLS.Enabled = true
@@ -1249,6 +1383,9 @@ func (s *Service) setupNode() error {
inboundOptions = opts inboundOptions = opts
s.logger.Info("Xboard Hysteria2 configured. Up: ", config.UpMbps, " Down: ", config.DownMbps, " IgnoreClientBW: ", config.Ignore_Client_Bandwidth) s.logger.Info("Xboard Hysteria2 configured. Up: ", config.UpMbps, " Down: ", config.DownMbps, " IgnoreClientBW: ", config.Ignore_Client_Bandwidth)
case "anytls": case "anytls":
if !hasUsableServerTLS(tlsOptions) {
return fmt.Errorf("anytls requires local TLS certificate or ACME; configure cert_mode file/content/http/dns or auto_tls with domain")
}
// V2bX: AnyTLS always uses TLS // V2bX: AnyTLS always uses TLS
anyTLS := tlsOptions anyTLS := tlsOptions
anyTLS.Enabled = true anyTLS.Enabled = true

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
) )
func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) { func TestXUserResolveKeyPrefersPasswordFields(t *testing.T) {
@@ -145,6 +146,72 @@ func TestExpandedNodeTagFallsBackToNodeID(t *testing.T) {
} }
} }
func TestApplyACMEConfigFromAutoTLS(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, nil, true, "example.com", 8443)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil {
t.Fatal("ACME options not configured")
}
if len(tlsOptions.ACME.Domain) != 1 || tlsOptions.ACME.Domain[0] != "example.com" {
t.Fatalf("ACME domains = %+v", tlsOptions.ACME.Domain)
}
if tlsOptions.ACME.AlternativeTLSPort != 8443 {
t.Fatalf("AlternativeTLSPort = %d, want 8443", tlsOptions.ACME.AlternativeTLSPort)
}
if !tlsOptions.ACME.DisableHTTPChallenge {
t.Fatal("DisableHTTPChallenge should be true for inline ACME")
}
}
func TestApplyACMEConfigFromDNSCertMode(t *testing.T) {
var tlsOptions option.InboundTLSOptions
ok := applyACMEConfig(&tlsOptions, &XCertConfig{
CertMode: "dns",
Domain: "example.com",
DNSProvider: "cloudflare",
DNSEnv: map[string]string{
"CF_API_TOKEN": "token",
},
}, false, "example.com", 443)
if !ok {
t.Fatal("applyACMEConfig() returned false")
}
if tlsOptions.ACME == nil || tlsOptions.ACME.DNS01Challenge == nil {
t.Fatal("DNS01Challenge not configured")
}
if tlsOptions.ACME.DNS01Challenge.Provider != "cloudflare" {
t.Fatalf("DNS provider = %q", tlsOptions.ACME.DNS01Challenge.Provider)
}
if tlsOptions.ACME.DNS01Challenge.CloudflareOptions.APIToken != "token" {
t.Fatalf("Cloudflare API token = %q", tlsOptions.ACME.DNS01Challenge.CloudflareOptions.APIToken)
}
if !tlsOptions.ACME.DisableTLSALPNChallenge {
t.Fatal("DisableTLSALPNChallenge should be true for dns mode")
}
}
func TestHasUsableServerTLS(t *testing.T) {
if hasUsableServerTLS(option.InboundTLSOptions{}) {
t.Fatal("empty TLS options should not be usable")
}
if !hasUsableServerTLS(option.InboundTLSOptions{
CertificatePath: "cert.pem",
KeyPath: "key.pem",
}) {
t.Fatal("file certificate should be usable")
}
if !hasUsableServerTLS(option.InboundTLSOptions{
ACME: &option.InboundACMEOptions{
Domain: badoption.Listable[string]{"example.com"},
},
}) {
t.Fatal("ACME certificate should be usable")
}
}
func TestBuildInboundMultiplex(t *testing.T) { func TestBuildInboundMultiplex(t *testing.T) {
config := &XMultiplexConfig{ config := &XMultiplexConfig{
Enabled: true, Enabled: true,