//go:build with_acme package tls import ( "context" "crypto/tls" "os" "slices" "strings" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service/filemanager" "github.com/caddyserver/certmagic" "github.com/libdns/acmedns" "github.com/libdns/alidns" "github.com/libdns/cloudflare" "github.com/mholt/acmez/v3/acme" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) type acmeWrapper struct { ctx context.Context cfg *certmagic.Config cache *certmagic.Cache zapLogger *zap.Logger dataDirectory string domain []string } func (w *acmeWrapper) Start() error { if w.dataDirectory != "" { err := filemanager.MkdirAll(w.ctx, w.dataDirectory, 0o700) if err != nil { return E.Cause(err, "create ACME data directory") } } config := w.cfg cache := certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) { return config, nil }, Logger: w.zapLogger, }) config = certmagic.New(cache, *config) w.cfg = config w.cache = cache return w.cfg.ManageSync(w.ctx, w.domain) } func (w *acmeWrapper) Close() error { if w.cache != nil { w.cache.Stop() } return nil } func (w *acmeWrapper) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { return w.cfg.GetCertificate(hello) } func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { var acmeServer string switch options.Provider { case "", "letsencrypt": acmeServer = certmagic.LetsEncryptProductionCA case "zerossl": acmeServer = certmagic.ZeroSSLProductionCA default: if !strings.HasPrefix(options.Provider, "https://") { return nil, nil, E.New("unsupported acme provider: " + options.Provider) } acmeServer = options.Provider } var ( storage certmagic.Storage dataDirectory string ) if options.DataDirectory != "" { dataDirectory = filemanager.BasePath(ctx, os.ExpandEnv(options.DataDirectory)) storage = &certmagic.FileStorage{ Path: dataDirectory, } } else { storage = certmagic.Default.Storage } zapLogger := zap.New(zapcore.NewCore( zapcore.NewConsoleEncoder(ACMEEncoderConfig()), &ACMELogWriter{Logger: logger}, zap.DebugLevel, )) config := &certmagic.Config{ DefaultServerName: options.DefaultServerName, Storage: storage, Logger: zapLogger, } profile := options.Profile if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA && slices.ContainsFunc(options.Domain, certmagic.SubjectIsIP) { profile = "shortlived" } acmeConfig := certmagic.ACMEIssuer{ CA: acmeServer, Email: options.Email, Agreed: true, Profile: profile, DisableHTTPChallenge: options.DisableHTTPChallenge, DisableTLSALPNChallenge: options.DisableTLSALPNChallenge, AltHTTPPort: int(options.AlternativeHTTPPort), AltTLSALPNPort: int(options.AlternativeTLSPort), Logger: zapLogger, } if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" { var solver certmagic.DNS01Solver switch dnsOptions.Provider { case C.DNSProviderAliDNS: solver.DNSProvider = &alidns.Provider{ CredentialInfo: alidns.CredentialInfo{ AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID, AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret, RegionID: dnsOptions.AliDNSOptions.RegionID, SecurityToken: dnsOptions.AliDNSOptions.SecurityToken, }, } case C.DNSProviderCloudflare: solver.DNSProvider = &cloudflare.Provider{ APIToken: dnsOptions.CloudflareOptions.APIToken, ZoneToken: dnsOptions.CloudflareOptions.ZoneToken, } case C.DNSProviderACMEDNS: solver.DNSProvider = &acmedns.Provider{ Username: dnsOptions.ACMEDNSOptions.Username, Password: dnsOptions.ACMEDNSOptions.Password, Subdomain: dnsOptions.ACMEDNSOptions.Subdomain, ServerURL: dnsOptions.ACMEDNSOptions.ServerURL, } default: return nil, nil, E.New("unsupported ACME DNS01 provider type: " + dnsOptions.Provider) } acmeConfig.DNS01Solver = &solver } if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" { acmeConfig.ExternalAccount = (*acme.EAB)(options.ExternalAccount) } config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeConfig)} wrapper := &acmeWrapper{ ctx: ctx, cfg: config, zapLogger: zapLogger, dataDirectory: dataDirectory, domain: options.Domain, } var tlsConfig *tls.Config if acmeConfig.DisableTLSALPNChallenge || acmeConfig.DNS01Solver != nil { tlsConfig = &tls.Config{ GetCertificate: wrapper.GetCertificate, } } else { tlsConfig = &tls.Config{ GetCertificate: wrapper.GetCertificate, NextProtos: []string{C.ACMETLS1Protocol}, } } return tlsConfig, wrapper, nil }