添加证书自动签发
This commit is contained in:
@@ -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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user