添加证书自动签发
This commit is contained in:
@@ -89,7 +89,7 @@ build_sing_box() {
|
||||
# Build params from Makefile
|
||||
VERSION=$(git rev-parse --short HEAD 2>/dev/null || echo "custom")
|
||||
# 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}Note: Using -p 1 to save memory and avoid silent crashes.${NC}"
|
||||
|
||||
@@ -184,6 +184,9 @@ type XInnerConfig struct {
|
||||
StreamSettings json.RawMessage `json:"streamSettings"`
|
||||
UpMbps int `json:"up_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 {
|
||||
@@ -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 {
|
||||
tlsSettings := inner.TLSSettings
|
||||
if tlsSettings == nil {
|
||||
@@ -880,6 +971,15 @@ func (s *Service) setupNode() error {
|
||||
if len(inner.NetworkSettings_) == 0 {
|
||||
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
|
||||
protocol := inner.Protocol
|
||||
@@ -950,9 +1050,31 @@ func (s *Service) setupNode() error {
|
||||
if tlsSettings != nil && tlsSettings.ServerName != "" {
|
||||
s.vlessServerName = tlsSettings.ServerName
|
||||
}
|
||||
hasCertificate := applyCertConfig(&tlsOptions, config.CertConfig)
|
||||
if config.CertConfig != nil && !hasCertificate && config.CertConfig.CertMode != "" && config.CertConfig.CertMode != "none" {
|
||||
s.logger.Warn("Xboard cert_config present but unsupported or incomplete for local TLS. cert_mode=", config.CertConfig.CertMode)
|
||||
certConfig := mergedCertConfig(inner, config)
|
||||
hasCertificate := applyCertConfig(&tlsOptions, certConfig)
|
||||
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 {
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -1163,6 +1285,9 @@ func (s *Service) setupNode() error {
|
||||
|
||||
inboundOptions = ssOptions
|
||||
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
|
||||
transport, err := getInboundTransport(networkType, networkSettings)
|
||||
if err != nil {
|
||||
@@ -1181,6 +1306,9 @@ func (s *Service) setupNode() error {
|
||||
}
|
||||
inboundOptions = opts
|
||||
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
|
||||
tuicTLS := tlsOptions
|
||||
tuicTLS.Enabled = true
|
||||
@@ -1202,6 +1330,9 @@ func (s *Service) setupNode() error {
|
||||
inboundOptions = opts
|
||||
s.logger.Info("Xboard TUIC configured. CongestionControl: ", congestionControl)
|
||||
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
|
||||
hyTLS := tlsOptions
|
||||
hyTLS.Enabled = true
|
||||
@@ -1218,6 +1349,9 @@ func (s *Service) setupNode() error {
|
||||
inboundOptions = opts
|
||||
s.logger.Info("Xboard Hysteria configured. Up: ", config.UpMbps, " Down: ", config.DownMbps)
|
||||
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
|
||||
hy2TLS := tlsOptions
|
||||
hy2TLS.Enabled = true
|
||||
@@ -1249,6 +1383,9 @@ func (s *Service) setupNode() error {
|
||||
inboundOptions = opts
|
||||
s.logger.Info("Xboard Hysteria2 configured. Up: ", config.UpMbps, " Down: ", config.DownMbps, " IgnoreClientBW: ", config.Ignore_Client_Bandwidth)
|
||||
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
|
||||
anyTLS := tlsOptions
|
||||
anyTLS.Enabled = true
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
)
|
||||
|
||||
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) {
|
||||
config := &XMultiplexConfig{
|
||||
Enabled: true,
|
||||
|
||||
Reference in New Issue
Block a user