Refactor ACME support to certificate provider

This commit is contained in:
nekohasekai
2026-03-23 20:04:36 +08:00
committed by 世界
parent f8b05790d1
commit e2727d9556
48 changed files with 3084 additions and 173 deletions

View File

@@ -0,0 +1,21 @@
package certificate
type Adapter struct {
providerType string
providerTag string
}
func NewAdapter(providerType string, providerTag string) Adapter {
return Adapter{
providerType: providerType,
providerTag: providerTag,
}
}
func (a *Adapter) Type() string {
return a.providerType
}
func (a *Adapter) Tag() string {
return a.providerTag
}

View File

@@ -0,0 +1,158 @@
package certificate
import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.CertificateProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.CertificateProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.CertificateProviderService
providerByTag map[string]adapter.CertificateProviderService
}
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.CertificateProviderService),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
providers := m.providers
m.providers = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, provider.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return err
}
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
m.access.Lock()
provider, found := m.providerByTag[tag]
m.access.Unlock()
return provider, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == provider
})
if index == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return provider.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = existsProvider.Close()
if err != nil {
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}

View File

@@ -0,0 +1,72 @@
package certificate
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error)
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
registry.register(providerType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
}
}
func (m *Registry) CreateOptions(providerType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[providerType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[providerType]
if !loaded {
return nil, E.New("certificate provider type not found: " + providerType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[providerType] = optionsConstructor
m.constructor[providerType] = constructor
}

View File

@@ -0,0 +1,38 @@
package adapter
import (
"context"
"crypto/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type CertificateProvider interface {
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
}
type ACMECertificateProvider interface {
CertificateProvider
GetACMENextProtos() []string
}
type CertificateProviderService interface {
Lifecycle
Type() string
Tag() string
CertificateProvider
}
type CertificateProviderRegistry interface {
option.CertificateProviderOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
}
type CertificateProviderManager interface {
Lifecycle
CertificateProviders() []CertificateProviderService
Get(tag string) (CertificateProviderService, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
}

123
box.go
View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
@@ -36,20 +37,21 @@ import (
var _ adapter.SimpleLifecycle = (*Box)(nil) var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct { type Box struct {
createdAt time.Time createdAt time.Time
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager network *route.NetworkManager
endpoint *endpoint.Manager endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
service *boxService.Manager service *boxService.Manager
dnsTransport *dns.TransportManager certificateProvider *boxCertificate.Manager
dnsRouter *dns.Router dnsTransport *dns.TransportManager
connection *route.ConnectionManager dnsRouter *dns.Router
router *route.Router connection *route.ConnectionManager
internalService []adapter.LifecycleService router *route.Router
done chan struct{} internalService []adapter.LifecycleService
done chan struct{}
} }
type Options struct { type Options struct {
@@ -65,6 +67,7 @@ func Context(
endpointRegistry adapter.EndpointRegistry, endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry, dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry, serviceRegistry adapter.ServiceRegistry,
certificateProviderRegistry adapter.CertificateProviderRegistry,
) context.Context { ) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -89,6 +92,10 @@ func Context(
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry) ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry) ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
} }
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
}
return ctx return ctx
} }
@@ -105,6 +112,7 @@ func New(options Options) (*Box, error) {
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if endpointRegistry == nil { if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context") return nil, E.New("missing endpoint registry in context")
@@ -121,6 +129,9 @@ func New(options Options) (*Box, error) {
if serviceRegistry == nil { if serviceRegistry == nil {
return nil, E.New("missing service registry in context") return nil, E.New("missing service registry in context")
} }
if certificateProviderRegistry == nil {
return nil, E.New("missing certificate provider registry in context")
}
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -178,11 +189,13 @@ func New(options Options) (*Box, error) {
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry) serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
@@ -271,6 +284,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]") return nil, E.Cause(err, "initialize inbound[", i, "]")
} }
} }
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var tag string var tag string
if outboundOptions.Tag != "" { if outboundOptions.Tag != "" {
@@ -297,22 +328,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "initialize outbound[", i, "]")
} }
} }
for i, serviceOptions := range options.Services { for i, certificateProviderOptions := range options.CertificateProviders {
var tag string var tag string
if serviceOptions.Tag != "" { if certificateProviderOptions.Tag != "" {
tag = serviceOptions.Tag tag = certificateProviderOptions.Tag
} else { } else {
tag = F.ToString(i) tag = F.ToString(i)
} }
err = serviceManager.Create( err = certificateProviderManager.Create(
ctx, ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")), logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
tag, tag,
serviceOptions.Type, certificateProviderOptions.Type,
serviceOptions.Options, certificateProviderOptions.Options,
) )
if err != nil { if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]") return nil, E.Cause(err, "initialize certificate provider[", i, "]")
} }
} }
outboundManager.Initialize(func() (adapter.Outbound, error) { outboundManager.Initialize(func() (adapter.Outbound, error) {
@@ -383,20 +414,21 @@ func New(options Options) (*Box, error) {
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service")) internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
} }
return &Box{ return &Box{
network: networkManager, network: networkManager,
endpoint: endpointManager, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
dnsTransport: dnsTransportManager, dnsTransport: dnsTransportManager,
service: serviceManager, service: serviceManager,
dnsRouter: dnsRouter, certificateProvider: certificateProviderManager,
connection: connectionManager, dnsRouter: dnsRouter,
router: router, connection: connectionManager,
createdAt: createdAt, router: router,
logFactory: logFactory, createdAt: createdAt,
logger: logFactory.Logger(), logFactory: logFactory,
internalService: internalServices, logger: logFactory.Logger(),
done: make(chan struct{}), internalService: internalServices,
done: make(chan struct{}),
}, nil }, nil
} }
@@ -450,7 +482,7 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
if err != nil { if err != nil {
return err return err
} }
@@ -470,11 +502,19 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -482,7 +522,7 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service) err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil { if err != nil {
return err return err
} }
@@ -506,8 +546,9 @@ func (s *Box) Close() error {
service adapter.Lifecycle service adapter.Lifecycle
}{ }{
{"service", s.service}, {"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound}, {"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"endpoint", s.endpoint},
{"outbound", s.outbound}, {"outbound", s.outbound},
{"router", s.router}, {"router", s.router},
{"connection", s.connection}, {"connection", s.connection},

View File

@@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error {
return nil return nil
} }
type acmeLogWriter struct {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string var acmeServer string
switch options.Provider { switch options.Provider {
@@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
storage = certmagic.Default.Storage storage = certmagic.Default.Storage
} }
zapLogger := zap.New(zapcore.NewCore( zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()), zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
&acmeLogWriter{logger: logger}, &ACMELogWriter{Logger: logger},
zap.DebugLevel, zap.DebugLevel,
)) ))
config := &certmagic.Config{ config := &certmagic.Config{
@@ -158,7 +127,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
} else { } else {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
GetCertificate: config.GetCertificate, GetCertificate: config.GetCertificate,
NextProtos: []string{ACMETLS1Protocol}, NextProtos: []string{C.ACMETLS1Protocol},
} }
} }
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil

41
common/tls/acme_logger.go Normal file
View File

@@ -0,0 +1,41 @@
package tls
import (
"strings"
"github.com/sagernet/sing/common/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type ACMELogWriter struct {
Logger logger.Logger
}
func (w *ACMELogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.Logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.Logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.Logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.Logger.Debug(logLine[7:])
default:
w.Logger.Debug(logLine)
}
return len(p), nil
}
func (w *ACMELogWriter) Sync() error {
return nil
}
func ACMEEncoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}

View File

@@ -32,6 +32,10 @@ type RealityServerConfig struct {
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
var tlsConfig utls.RealityConfig var tlsConfig utls.RealityConfig
if options.CertificateProvider != nil {
return nil, E.New("certificate_provider is unavailable in reality")
}
//nolint:staticcheck
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality") return nil, E.New("acme is unavailable in reality")
} }

View File

@@ -13,19 +13,87 @@ import (
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
) )
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
type managedCertificateProvider interface {
adapter.CertificateProvider
adapter.SimpleLifecycle
}
type sharedCertificateProvider struct {
tag string
manager adapter.CertificateProviderManager
provider adapter.CertificateProviderService
}
func (p *sharedCertificateProvider) Start() error {
provider, found := p.manager.Get(p.tag)
if !found {
return E.New("certificate provider not found: ", p.tag)
}
p.provider = provider
return nil
}
func (p *sharedCertificateProvider) Close() error {
return nil
}
func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *sharedCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
type inlineCertificateProvider struct {
provider adapter.CertificateProviderService
}
func (p *inlineCertificateProvider) Start() error {
for _, stage := range adapter.ListStartStages {
err := adapter.LegacyStart(p.provider, stage)
if err != nil {
return err
}
}
return nil
}
func (p *inlineCertificateProvider) Close() error {
return p.provider.Close()
}
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *inlineCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
func getACMENextProtos(provider adapter.CertificateProvider) []string {
if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME {
return acmeProvider.GetACMENextProtos()
}
return nil
}
type STDServerConfig struct { type STDServerConfig struct {
access sync.RWMutex access sync.RWMutex
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
certificateProvider managedCertificateProvider
acmeService adapter.SimpleLifecycle acmeService adapter.SimpleLifecycle
certificate []byte certificate []byte
key []byte key []byte
@@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
func (c *STDServerConfig) NextProtos() []string { func (c *STDServerConfig) NextProtos() []string {
c.access.RLock() c.access.RLock()
defer c.access.RUnlock() defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
return c.config.NextProtos[1:] return c.config.NextProtos[1:]
} else {
return c.config.NextProtos
} }
return c.config.NextProtos
} }
func (c *STDServerConfig) SetNextProtos(nextProto []string) { func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock() c.access.Lock()
defer c.access.Unlock() defer c.access.Unlock()
config := c.config.Clone() config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...) config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else { } else {
config.NextProtos = nextProto config.NextProtos = nextProto
@@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.config = config c.config = config
} }
func (c *STDServerConfig) hasACMEALPN() bool {
if c.acmeService != nil {
return true
}
if c.certificateProvider != nil {
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
return len(acmeProvider.GetACMENextProtos()) > 0
}
}
return false
}
func (c *STDServerConfig) STDConfig() (*STDConfig, error) { func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
return c.config, nil return c.config, nil
} }
@@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config {
} }
func (c *STDServerConfig) Start() error { func (c *STDServerConfig) Start() error {
if c.acmeService != nil { if c.certificateProvider != nil {
return c.acmeService.Start() err := c.certificateProvider.Start()
} else {
err := c.startWatcher()
if err != nil { if err != nil {
c.logger.Warn("create fsnotify watcher: ", err) return err
}
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
nextProtos := acmeProvider.GetACMENextProtos()
if len(nextProtos) > 0 {
c.access.Lock()
config := c.config.Clone()
mergedNextProtos := append([]string{}, nextProtos...)
for _, nextProto := range config.NextProtos {
if !common.Contains(mergedNextProtos, nextProto) {
mergedNextProtos = append(mergedNextProtos, nextProto)
}
}
config.NextProtos = mergedNextProtos
c.config = config
c.access.Unlock()
}
} }
return nil
} }
if c.acmeService != nil {
err := c.acmeService.Start()
if err != nil {
return err
}
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
return nil
} }
func (c *STDServerConfig) startWatcher() error { func (c *STDServerConfig) startWatcher() error {
@@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
} }
func (c *STDServerConfig) Close() error { func (c *STDServerConfig) Close() error {
if c.acmeService != nil { return common.Close(c.certificateProvider, c.acmeService, c.watcher)
return c.acmeService.Close()
}
if c.watcher != nil {
return c.watcher.Close()
}
return nil
} }
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) { func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled { if !options.Enabled {
return nil, nil return nil, nil
} }
//nolint:staticcheck
if options.CertificateProvider != nil && options.ACME != nil {
return nil, E.New("certificate_provider and acme are mutually exclusive")
}
var tlsConfig *tls.Config var tlsConfig *tls.Config
var certificateProvider managedCertificateProvider
var acmeService adapter.SimpleLifecycle var acmeService adapter.SimpleLifecycle
var err error var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.CertificateProvider != nil {
certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider)
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{
GetCertificate: certificateProvider.GetCertificate,
}
if options.Insecure {
return nil, errInsecureUnused
}
} else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck
deprecated.Report(ctx, deprecated.OptionInlineACME)
//nolint:staticcheck //nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME)) tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil { if err != nil {
@@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
certificate []byte certificate []byte
key []byte key []byte
) )
if acmeService == nil { if certificateProvider == nil && acmeService == nil {
if len(options.Certificate) > 0 { if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n")) certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" { } else if options.CertificatePath != "" {
@@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
serverConfig := &STDServerConfig{ serverConfig := &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
certificateProvider: certificateProvider,
acmeService: acmeService, acmeService: acmeService,
certificate: certificate, certificate: certificate,
key: key, key: key,
@@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
echKeyPath: echKeyPath, echKeyPath: echKeyPath,
} }
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) { serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock() serverConfig.access.RLock()
defer serverConfig.access.Unlock() defer serverConfig.access.RUnlock()
return serverConfig.config, nil return serverConfig.config, nil
} }
var config ServerConfig = serverConfig var config ServerConfig = serverConfig
@@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
} }
return config, nil return config, nil
} }
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
if options.IsShared() {
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
if manager == nil {
return nil, E.New("missing certificate provider manager in context")
}
return &sharedCertificateProvider{
tag: options.Tag,
manager: manager,
}, nil
}
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if registry == nil {
return nil, E.New("missing certificate provider registry in context")
}
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
if err != nil {
return nil, E.Cause(err, "create inline certificate provider")
}
return &inlineCertificateProvider{
provider: provider,
}, nil
}

View File

@@ -1,36 +1,38 @@
package constant package constant
const ( const (
TypeTun = "tun" TypeTun = "tun"
TypeRedirect = "redirect" TypeRedirect = "redirect"
TypeTProxy = "tproxy" TypeTProxy = "tproxy"
TypeDirect = "direct" TypeDirect = "direct"
TypeBlock = "block" TypeBlock = "block"
TypeDNS = "dns" TypeDNS = "dns"
TypeSOCKS = "socks" TypeSOCKS = "socks"
TypeHTTP = "http" TypeHTTP = "http"
TypeMixed = "mixed" TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks" TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess" TypeVMess = "vmess"
TypeTrojan = "trojan" TypeTrojan = "trojan"
TypeNaive = "naive" TypeNaive = "naive"
TypeWireGuard = "wireguard" TypeWireGuard = "wireguard"
TypeHysteria = "hysteria" TypeHysteria = "hysteria"
TypeTor = "tor" TypeTor = "tor"
TypeSSH = "ssh" TypeSSH = "ssh"
TypeShadowTLS = "shadowtls" TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls" TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr" TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless" TypeVLESS = "vless"
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale" TypeTailscale = "tailscale"
TypeDERP = "derp" TypeDERP = "derp"
TypeResolved = "resolved" TypeResolved = "resolved"
TypeSSMAPI = "ssm-api" TypeSSMAPI = "ssm-api"
TypeCCM = "ccm" TypeCCM = "ccm"
TypeOCM = "ocm" TypeOCM = "ocm"
TypeOOMKiller = "oom-killer" TypeOOMKiller = "oom-killer"
TypeACME = "acme"
TypeCloudflareOriginCA = "cloudflare-origin-ca"
) )
const ( const (

View File

@@ -1,3 +1,3 @@
package tls package constant
const ACMETLS1Protocol = "acme-tls/1" const ACMETLS1Protocol = "acme-tls/1"

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0" !!! quote "Changes in sing-box 1.14.0"
:material-plus: [include_mac_address](#include_mac_address) :material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address) :material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "Changes in sing-box 1.13.3" !!! quote "Changes in sing-box 1.13.3"

View File

@@ -1,7 +1,6 @@
# Introduction # Introduction
sing-box uses JSON for configuration files. sing-box uses JSON for configuration files.
### Structure ### Structure
```json ```json
@@ -10,6 +9,7 @@ sing-box uses JSON for configuration files.
"dns": {}, "dns": {},
"ntp": {}, "ntp": {},
"certificate": {}, "certificate": {},
"certificate_providers": [],
"endpoints": [], "endpoints": [],
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
@@ -27,6 +27,7 @@ sing-box uses JSON for configuration files.
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) | | `ntp` | [NTP](./ntp/) |
| `certificate` | [Certificate](./certificate/) | | `certificate` | [Certificate](./certificate/) |
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
| `endpoints` | [Endpoint](./endpoint/) | | `endpoints` | [Endpoint](./endpoint/) |
| `inbounds` | [Inbound](./inbound/) | | `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) | | `outbounds` | [Outbound](./outbound/) |
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
```bash ```bash
sing-box merge output.json -c config.json -D config_directory sing-box merge output.json -c config.json -D config_directory
``` ```

View File

@@ -1,7 +1,6 @@
# 引言 # 引言
sing-box 使用 JSON 作为配置文件格式。 sing-box 使用 JSON 作为配置文件格式。
### 结构 ### 结构
```json ```json
@@ -10,6 +9,7 @@ sing-box 使用 JSON 作为配置文件格式。
"dns": {}, "dns": {},
"ntp": {}, "ntp": {},
"certificate": {}, "certificate": {},
"certificate_providers": [],
"endpoints": [], "endpoints": [],
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
@@ -27,6 +27,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `dns` | [DNS](./dns/) | | `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) | | `ntp` | [NTP](./ntp/) |
| `certificate` | [证书](./certificate/) | | `certificate` | [证书](./certificate/) |
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
| `endpoints` | [端点](./endpoint/) | | `endpoints` | [端点](./endpoint/) |
| `inbounds` | [入站](./inbound/) | | `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) | | `outbounds` | [出站](./outbound/) |
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
```bash ```bash
sing-box merge output.json -c config.json -D config_directory sing-box merge output.json -c config.json -D config_directory
``` ```

View File

@@ -0,0 +1,150 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [detour](#detour)
# ACME
!!! quote ""
`with_acme` build tag required.
### Structure
```json
{
"type": "acme",
"tag": "",
"domain": [],
"data_directory": "",
"default_server_name": "",
"email": "",
"provider": "",
"account_key": "",
"disable_http_challenge": false,
"disable_tls_alpn_challenge": false,
"alternative_http_port": 0,
"alternative_tls_port": 0,
"external_account": {
"key_id": "",
"mac_key": ""
},
"dns01_challenge": {},
"key_type": "",
"detour": ""
}
```
### Fields
#### domain
==Required==
List of domains.
#### data_directory
The directory to store ACME data.
`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty.
#### default_server_name
Server name to use when choosing a certificate if the ClientHello's ServerName field is empty.
#### email
The email address to use when creating or selecting an existing ACME server account.
#### provider
The ACME CA provider to use.
| Value | Provider |
|-------------------------|---------------|
| `letsencrypt (default)` | Let's Encrypt |
| `zerossl` | ZeroSSL |
| `https://...` | Custom |
When `provider` is `zerossl`, sing-box will automatically request ZeroSSL EAB credentials if `email` is set and
`external_account` is empty.
When `provider` is `zerossl`, at least one of `external_account`, `email`, or `account_key` is required.
#### account_key
!!! question "Since sing-box 1.14.0"
The PEM-encoded private key of an existing ACME account.
#### disable_http_challenge
Disable all HTTP challenges.
#### disable_tls_alpn_challenge
Disable all TLS-ALPN challenges
#### alternative_http_port
The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a
listener for the HTTP challenge.
#### alternative_tls_port
The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to
succeed.
#### external_account
EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known
by the CA.
External account bindings are used to associate an ACME account with an existing account in a non-ACME system, such as
a CA customer database.
To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a
key identifier, using some mechanism outside of ACME. §7.3.4
#### external_account.key_id
The key identifier.
#### external_account.mac_key
The MAC key.
#### dns01_challenge
ACME DNS01 challenge field. If configured, other challenge methods will be disabled.
See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details.
#### key_type
!!! question "Since sing-box 1.14.0"
The private key type to generate for new certificates.
| Value | Type |
|------------|---------|
| `ed25519` | Ed25519 |
| `p256` | P-256 |
| `p384` | P-384 |
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### detour
!!! question "Since sing-box 1.14.0"
The tag of the upstream outbound.
All provider HTTP requests will use this outbound.

View File

@@ -0,0 +1,145 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [detour](#detour)
# ACME
!!! quote ""
需要 `with_acme` 构建标签。
### 结构
```json
{
"type": "acme",
"tag": "",
"domain": [],
"data_directory": "",
"default_server_name": "",
"email": "",
"provider": "",
"account_key": "",
"disable_http_challenge": false,
"disable_tls_alpn_challenge": false,
"alternative_http_port": 0,
"alternative_tls_port": 0,
"external_account": {
"key_id": "",
"mac_key": ""
},
"dns01_challenge": {},
"key_type": "",
"detour": ""
}
```
### 字段
#### domain
==必填==
域名列表。
#### data_directory
ACME 数据存储目录。
如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`
#### default_server_name
如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。
#### email
创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。
#### provider
要使用的 ACME CA 提供商。
| 值 | 提供商 |
|--------------------|---------------|
| `letsencrypt (默认)` | Let's Encrypt |
| `zerossl` | ZeroSSL |
| `https://...` | 自定义 |
`provider``zerossl` 时,如果设置了 `email` 且未设置 `external_account`
sing-box 会自动向 ZeroSSL 请求 EAB 凭据。
`provider``zerossl` 时,必须至少设置 `external_account``email``account_key` 之一。
#### account_key
!!! question "自 sing-box 1.14.0 起"
现有 ACME 帐户的 PEM 编码私钥。
#### disable_http_challenge
禁用所有 HTTP 质询。
#### disable_tls_alpn_challenge
禁用所有 TLS-ALPN 质询。
#### alternative_http_port
用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。
#### alternative_tls_port
用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。
#### external_account
EAB外部帐户绑定包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。
外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。
为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4
#### external_account.key_id
密钥标识符。
#### external_account.mac_key
MAC 密钥。
#### dns01_challenge
ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
参阅 [DNS01 质询字段](/zh/configuration/shared/dns01_challenge/)。
#### key_type
!!! question "自 sing-box 1.14.0 起"
为新证书生成的私钥类型。
| 值 | 类型 |
|-----------|----------|
| `ed25519` | Ed25519 |
| `p256` | P-256 |
| `p384` | P-384 |
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### detour
!!! question "自 sing-box 1.14.0 起"
上游出站的标签。
所有提供者 HTTP 请求将使用此出站。

View File

@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Cloudflare Origin CA
### Structure
```json
{
"type": "cloudflare-origin-ca",
"tag": "",
"domain": [],
"data_directory": "",
"api_token": "",
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"detour": ""
}
```
### Fields
#### domain
==Required==
List of domain names or wildcard domain names to include in the certificate.
#### data_directory
Root directory used to store the issued certificate, private key, and metadata.
If empty, sing-box uses the same default data directory as the ACME certificate provider:
`$XDG_DATA_HOME/certmagic` or `$HOME/.local/share/certmagic`.
#### api_token
Cloudflare API token used to create the certificate.
Get or create one in [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens).
Requires the `Zone / SSL and Certificates / Edit` permission.
Conflict with `origin_ca_key`.
#### origin_ca_key
Cloudflare Origin CA Key.
Get it in [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens).
Conflict with `api_token`.
#### request_type
The signature type to request from Cloudflare.
| Value | Type |
|----------------------|-------------|
| `origin-rsa` | RSA |
| `origin-ecc` | ECDSA P-256 |
`origin-rsa` is used if empty.
#### requested_validity
The requested certificate validity in days.
Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
`5475` days (15 years) is used if empty.
#### detour
The tag of the upstream outbound.
All provider HTTP requests will use this outbound.

View File

@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# Cloudflare Origin CA
### 结构
```json
{
"type": "cloudflare-origin-ca",
"tag": "",
"domain": [],
"data_directory": "",
"api_token": "",
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"detour": ""
}
```
### 字段
#### domain
==必填==
要写入证书的域名或通配符域名列表。
#### data_directory
保存签发证书、私钥和元数据的根目录。
如果为空sing-box 会使用与 ACME 证书提供者相同的默认数据目录:
`$XDG_DATA_HOME/certmagic``$HOME/.local/share/certmagic`
#### api_token
用于创建证书的 Cloudflare API Token。
可在 [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens) 获取或创建。
需要 `Zone / SSL and Certificates / Edit` 权限。
`origin_ca_key` 冲突。
#### origin_ca_key
Cloudflare Origin CA Key。
可在 [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens) 获取。
`api_token` 冲突。
#### request_type
向 Cloudflare 请求的签名类型。
| 值 | 类型 |
|----------------------|-------------|
| `origin-rsa` | RSA |
| `origin-ecc` | ECDSA P-256 |
如果为空,使用 `origin-rsa`
#### requested_validity
请求的证书有效期,单位为天。
可用值:`7``30``90``365``730``1095``5475`
如果为空,使用 `5475`15 年)。
#### detour
上游出站的标签。
所有提供者 HTTP 请求将使用此出站。

View File

@@ -0,0 +1,32 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Certificate Provider
### Structure
```json
{
"certificate_providers": [
{
"type": "",
"tag": ""
}
]
}
```
### Fields
| Type | Format |
|--------|------------------|
| `acme` | [ACME](/configuration/shared/certificate-provider/acme) |
| `tailscale` | [Tailscale](/configuration/shared/certificate-provider/tailscale) |
| `cloudflare-origin-ca` | [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca) |
#### tag
The tag of the certificate provider.

View File

@@ -0,0 +1,32 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# 证书提供者
### 结构
```json
{
"certificate_providers": [
{
"type": "",
"tag": ""
}
]
}
```
### 字段
| 类型 | 格式 |
|--------|------------------|
| `acme` | [ACME](/zh/configuration/shared/certificate-provider/acme) |
| `tailscale` | [Tailscale](/zh/configuration/shared/certificate-provider/tailscale) |
| `cloudflare-origin-ca` | [Cloudflare Origin CA](/zh/configuration/shared/certificate-provider/cloudflare-origin-ca) |
#### tag
证书提供者的标签。

View File

@@ -0,0 +1,27 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Tailscale
### Structure
```json
{
"type": "tailscale",
"tag": "ts-cert",
"endpoint": "ts-ep"
}
```
### Fields
#### endpoint
==Required==
The tag of the [Tailscale endpoint](/configuration/endpoint/tailscale/) to reuse.
[MagicDNS and HTTPS](https://tailscale.com/kb/1153/enabling-https) must be enabled in the Tailscale admin console.

View File

@@ -0,0 +1,27 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# Tailscale
### 结构
```json
{
"type": "tailscale",
"tag": "ts-cert",
"endpoint": "ts-ep"
}
```
### 字段
#### endpoint
==必填==
要复用的 [Tailscale 端点](/zh/configuration/endpoint/tailscale/) 的标签。
必须在 Tailscale 管理控制台中启用 [MagicDNS 和 HTTPS](https://tailscale.com/kb/1153/enabling-https)。

View File

@@ -2,6 +2,14 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [ttl](#ttl)
:material-plus: [propagation_delay](#propagation_delay)
:material-plus: [propagation_timeout](#propagation_timeout)
:material-plus: [resolvers](#resolvers)
:material-plus: [override_domain](#override_domain)
!!! quote "Changes in sing-box 1.13.0" !!! quote "Changes in sing-box 1.13.0"
:material-plus: [alidns.security_token](#security_token) :material-plus: [alidns.security_token](#security_token)
@@ -12,12 +20,57 @@ icon: material/new-box
```json ```json
{ {
"ttl": "",
"propagation_delay": "",
"propagation_timeout": "",
"resolvers": [],
"override_domain": "",
"provider": "", "provider": "",
... // Provider Fields ... // Provider Fields
} }
``` ```
### Fields
#### ttl
!!! question "Since sing-box 1.14.0"
The TTL of the temporary TXT record used for the DNS challenge.
#### propagation_delay
!!! question "Since sing-box 1.14.0"
How long to wait after creating the challenge record before starting propagation checks.
#### propagation_timeout
!!! question "Since sing-box 1.14.0"
The maximum time to wait for the challenge record to propagate.
Set to `-1` to disable propagation checks.
#### resolvers
!!! question "Since sing-box 1.14.0"
Preferred DNS resolvers to use for DNS propagation checks.
#### override_domain
!!! question "Since sing-box 1.14.0"
Override the domain name used for the DNS challenge record.
Useful when `_acme-challenge` is delegated to a different zone.
#### provider
The DNS provider. See below for provider-specific fields.
### Provider Fields ### Provider Fields
#### Alibaba Cloud DNS #### Alibaba Cloud DNS

View File

@@ -2,6 +2,14 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [ttl](#ttl)
:material-plus: [propagation_delay](#propagation_delay)
:material-plus: [propagation_timeout](#propagation_timeout)
:material-plus: [resolvers](#resolvers)
:material-plus: [override_domain](#override_domain)
!!! quote "sing-box 1.13.0 中的更改" !!! quote "sing-box 1.13.0 中的更改"
:material-plus: [alidns.security_token](#security_token) :material-plus: [alidns.security_token](#security_token)
@@ -12,12 +20,57 @@ icon: material/new-box
```json ```json
{ {
"ttl": "",
"propagation_delay": "",
"propagation_timeout": "",
"resolvers": [],
"override_domain": "",
"provider": "", "provider": "",
... // 提供商字段 ... // 提供商字段
} }
``` ```
### 字段
#### ttl
!!! question "自 sing-box 1.14.0 起"
DNS 质询临时 TXT 记录的 TTL。
#### propagation_delay
!!! question "自 sing-box 1.14.0 起"
创建质询记录后,在开始传播检查前要等待的时间。
#### propagation_timeout
!!! question "自 sing-box 1.14.0 起"
等待质询记录传播完成的最长时间。
设为 `-1` 可禁用传播检查。
#### resolvers
!!! question "自 sing-box 1.14.0 起"
进行 DNS 传播检查时优先使用的 DNS 解析器。
#### override_domain
!!! question "自 sing-box 1.14.0 起"
覆盖 DNS 质询记录使用的域名。
适用于将 `_acme-challenge` 委托到其他 zone 的场景。
#### provider
DNS 提供商。提供商专有字段见下文。
### 提供商字段 ### 提供商字段
#### Alibaba Cloud DNS #### Alibaba Cloud DNS

View File

@@ -2,6 +2,11 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [certificate_provider](#certificate_provider)
:material-delete-clock: [acme](#acme-fields)
!!! quote "Changes in sing-box 1.13.0" !!! quote "Changes in sing-box 1.13.0"
:material-plus: [kernel_tx](#kernel_tx) :material-plus: [kernel_tx](#kernel_tx)
@@ -49,6 +54,10 @@ icon: material/new-box
"key_path": "", "key_path": "",
"kernel_tx": false, "kernel_tx": false,
"kernel_rx": false, "kernel_rx": false,
"certificate_provider": "",
// Deprecated
"acme": { "acme": {
"domain": [], "domain": [],
"data_directory": "", "data_directory": "",
@@ -408,6 +417,18 @@ Enable kernel TLS transmit support.
Enable kernel TLS receive support. Enable kernel TLS receive support.
#### certificate_provider
!!! question "Since sing-box 1.14.0"
==Server only==
A string or an object.
When string, the tag of a shared [Certificate Provider](/configuration/shared/certificate-provider/).
When object, an inline certificate provider. See [Certificate Provider](/configuration/shared/certificate-provider/) for available types and fields.
## Custom TLS support ## Custom TLS support
!!! info "QUIC support" !!! info "QUIC support"
@@ -469,7 +490,7 @@ The ECH key and configuration can be generated by `sing-box generate ech-keypair
!!! failure "Deprecated in sing-box 1.12.0" !!! failure "Deprecated in sing-box 1.12.0"
ECH support has been migrated to use stdlib in sing-box 1.12.0, which does not come with support for PQ signature schemes, so `pq_signature_schemes_enabled` has been deprecated and no longer works. `pq_signature_schemes_enabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0.
Enable support for post-quantum peer certificate signature schemes. Enable support for post-quantum peer certificate signature schemes.
@@ -477,7 +498,7 @@ Enable support for post-quantum peer certificate signature schemes.
!!! failure "Deprecated in sing-box 1.12.0" !!! failure "Deprecated in sing-box 1.12.0"
`dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works. `dynamic_record_sizing_disabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0.
Disables adaptive sizing of TLS records. Disables adaptive sizing of TLS records.
@@ -566,6 +587,10 @@ Fragment TLS handshake into multiple TLS records to bypass firewalls.
### ACME Fields ### ACME Fields
!!! failure "Deprecated in sing-box 1.14.0"
Inline ACME options are deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
#### domain #### domain
List of domain. List of domain.
@@ -677,4 +702,4 @@ A hexadecimal string with zero to eight digits.
The maximum time difference between the server and the client. The maximum time difference between the server and the client.
Check disabled if empty. Check disabled if empty.

View File

@@ -2,6 +2,11 @@
icon: material/new-box icon: material/new-box
--- ---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [certificate_provider](#certificate_provider)
:material-delete-clock: [acme](#acme-字段)
!!! quote "sing-box 1.13.0 中的更改" !!! quote "sing-box 1.13.0 中的更改"
:material-plus: [kernel_tx](#kernel_tx) :material-plus: [kernel_tx](#kernel_tx)
@@ -49,6 +54,10 @@ icon: material/new-box
"key_path": "", "key_path": "",
"kernel_tx": false, "kernel_tx": false,
"kernel_rx": false, "kernel_rx": false,
"certificate_provider": "",
// 废弃的
"acme": { "acme": {
"domain": [], "domain": [],
"data_directory": "", "data_directory": "",
@@ -407,6 +416,18 @@ echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/
启用内核 TLS 接收支持。 启用内核 TLS 接收支持。
#### certificate_provider
!!! question "自 sing-box 1.14.0 起"
==仅服务器==
字符串或对象。
为字符串时,共享[证书提供者](/zh/configuration/shared/certificate-provider/)的标签。
为对象时,内联的证书提供者。可用类型和字段参阅[证书提供者](/zh/configuration/shared/certificate-provider/)。
## 自定义 TLS 支持 ## 自定义 TLS 支持
!!! info "QUIC 支持" !!! info "QUIC 支持"
@@ -465,7 +486,7 @@ ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。
!!! failure "已在 sing-box 1.12.0 废弃" !!! failure "已在 sing-box 1.12.0 废弃"
ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支持后量子对等证书签名方案,因此 `pq_signature_schemes_enabled` 已被弃用且不再工作 `pq_signature_schemes_enabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除
启用对后量子对等证书签名方案的支持。 启用对后量子对等证书签名方案的支持。
@@ -473,7 +494,7 @@ ECH 密钥和配置可以通过 `sing-box generate ech-keypair` 生成。
!!! failure "已在 sing-box 1.12.0 废弃" !!! failure "已在 sing-box 1.12.0 废弃"
`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作 `dynamic_record_sizing_disabled` 已在 sing-box 1.12.0 废弃且已在 sing-box 1.13.0 中被移除
禁用 TLS 记录的自适应大小调整。 禁用 TLS 记录的自适应大小调整。
@@ -561,6 +582,10 @@ ECH 配置路径PEM 格式。
### ACME 字段 ### ACME 字段
!!! failure "已在 sing-box 1.14.0 废弃"
内联 ACME 选项已在 sing-box 1.14.0 废弃且将在 sing-box 1.16.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。
#### domain #### domain
域名列表。 域名列表。

View File

@@ -4,6 +4,16 @@ icon: material/delete-alert
# Deprecated Feature List # Deprecated Feature List
## 1.14.0
#### Inline ACME options in TLS
Inline ACME options (`tls.acme`) are deprecated
and can be replaced by the ACME certificate provider,
check [Migration](../migration/#migrate-inline-acme-to-certificate-provider).
Old fields will be removed in sing-box 1.16.0.
## 1.12.0 ## 1.12.0
#### Legacy DNS server formats #### Legacy DNS server formats
@@ -28,7 +38,7 @@ so `pq_signature_schemes_enabled` has been deprecated and no longer works.
Also, `dynamic_record_sizing_disabled` has nothing to do with ECH, Also, `dynamic_record_sizing_disabled` has nothing to do with ECH,
was added by mistake, has been deprecated and no longer works. was added by mistake, has been deprecated and no longer works.
These fields will be removed in sing-box 1.13.0. These fields were removed in sing-box 1.13.0.
## 1.11.0 ## 1.11.0
@@ -38,7 +48,7 @@ Legacy special outbounds (`block` / `dns`) are deprecated
and can be replaced by rule actions, and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions). check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions).
Old fields will be removed in sing-box 1.13.0. Old fields were removed in sing-box 1.13.0.
#### Legacy inbound fields #### Legacy inbound fields
@@ -46,7 +56,7 @@ Legacy inbound fields `inbound.<sniff/domain_strategy/...>` are deprecated
and can be replaced by rule actions, and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions).
Old fields will be removed in sing-box 1.13.0. Old fields were removed in sing-box 1.13.0.
#### Destination override fields in direct outbound #### Destination override fields in direct outbound
@@ -54,18 +64,20 @@ Destination override fields (`override_address` / `override_port`) in direct out
and can be replaced by rule actions, and can be replaced by rule actions,
check [Migration](../migration/#migrate-destination-override-fields-to-route-options). check [Migration](../migration/#migrate-destination-override-fields-to-route-options).
Old fields were removed in sing-box 1.13.0.
#### WireGuard outbound #### WireGuard outbound
WireGuard outbound is deprecated and can be replaced by endpoint, WireGuard outbound is deprecated and can be replaced by endpoint,
check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint).
Old outbound will be removed in sing-box 1.13.0. Old outbound was removed in sing-box 1.13.0.
#### GSO option in TUN #### GSO option in TUN
GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN. GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN.
Old fields will be removed in sing-box 1.13.0. Old fields were removed in sing-box 1.13.0.
## 1.10.0 ## 1.10.0
@@ -75,12 +87,12 @@ Old fields will be removed in sing-box 1.13.0.
`inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_address` and `inet6_route_address` are merged into `route_address`,
`inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`.
Old fields will be removed in sing-box 1.12.0. Old fields were removed in sing-box 1.12.0.
#### Match source rule items are renamed #### Match source rule items are renamed
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to `rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0. `rule_set_ip_cidr_match_source` and were removed in sing-box 1.11.0.
#### Drop support for go1.18 and go1.19 #### Drop support for go1.18 and go1.19
@@ -95,7 +107,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o
#### GeoIP #### GeoIP
GeoIP is deprecated and will be removed in sing-box 1.12.0. GeoIP is deprecated and was removed in sing-box 1.12.0.
The maxmind GeoIP National Database, as an IP classification database, The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing, is not entirely suitable for traffic bypassing,
@@ -106,7 +118,7 @@ check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite #### Geosite
Geosite is deprecated and will be removed in sing-box 1.12.0. Geosite is deprecated and was removed in sing-box 1.12.0.
Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution,
suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management.

View File

@@ -4,6 +4,18 @@ icon: material/delete-alert
# 废弃功能列表 # 废弃功能列表
## 1.14.0
#### TLS 中的内联 ACME 选项
TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
且可以通过 ACME 证书提供者替代,
参阅 [迁移指南](/zh/migration/#迁移内联-acme-到证书提供者)。
旧字段将在 sing-box 1.16.0 中被移除。
## 1.12.0
#### 旧的 DNS 服务器格式 #### 旧的 DNS 服务器格式
DNS 服务器已重构, DNS 服务器已重构,
@@ -24,7 +36,7 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支
另外,`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。 另外,`dynamic_record_sizing_disabled` 与 ECH 无关,是错误添加的,现已弃用且不再工作。
相关字段在 sing-box 1.13.0 中被移除。 相关字段在 sing-box 1.13.0 中被移除。
## 1.11.0 ## 1.11.0
@@ -33,41 +45,41 @@ ECH 支持已在 sing-box 1.12.0 迁移至使用标准库,但标准库不支
旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, 旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作)。
旧字段在 sing-box 1.13.0 中被移除。 旧字段在 sing-box 1.13.0 中被移除。
#### 旧的入站字段 #### 旧的入站字段
旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代, 旧的入站字段(`inbound.<sniff/domain_strategy/...>`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。 参阅 [迁移指南](/zh/migration/#迁移旧的入站字段到规则动作)。
旧字段在 sing-box 1.13.0 中被移除。 旧字段在 sing-box 1.13.0 中被移除。
#### direct 出站中的目标地址覆盖字段 #### direct 出站中的目标地址覆盖字段
direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代,
参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。 参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
旧字段在 sing-box 1.13.0 中被移除。 旧字段在 sing-box 1.13.0 中被移除。
#### WireGuard 出站 #### WireGuard 出站
WireGuard 出站已废弃且可以通过端点替代, WireGuard 出站已废弃且可以通过端点替代,
参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。 参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
旧出站在 sing-box 1.13.0 中被移除。 旧出站在 sing-box 1.13.0 中被移除。
#### TUN 的 GSO 字段 #### TUN 的 GSO 字段
GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。 GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。
旧字段在 sing-box 1.13.0 中被移除。 旧字段在 sing-box 1.13.0 中被移除。
## 1.10.0 ## 1.10.0
#### Match source 规则项已重命名 #### Match source 规则项已重命名
`rule_set_ipcidr_match_source` 路由和 DNS 规则项已被重命名为 `rule_set_ipcidr_match_source` 路由和 DNS 规则项已被重命名为
`rule_set_ip_cidr_match_source`在 sing-box 1.11.0 中被移除。 `rule_set_ip_cidr_match_source`在 sing-box 1.11.0 中被移除。
#### TUN 地址字段已合并 #### TUN 地址字段已合并
@@ -75,7 +87,7 @@ GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用
`inet4_route_address``inet6_route_address` 已合并为 `route_address` `inet4_route_address``inet6_route_address` 已合并为 `route_address`
`inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address` `inet4_route_exclude_address``inet6_route_exclude_address` 已合并为 `route_exclude_address`
旧字段在 sing-box 1.11.0 中被移除。 旧字段在 sing-box 1.12.0 中被移除。
#### 移除对 go1.18 和 go1.19 的支持 #### 移除对 go1.18 和 go1.19 的支持
@@ -90,7 +102,7 @@ Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 `
#### GeoIP #### GeoIP
GeoIP 已废弃且在 sing-box 1.12.0 中被移除。 GeoIP 已废弃且在 sing-box 1.12.0 中被移除。
maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过,
且现有的实现均存在内存使用大与管理困难的问题。 且现有的实现均存在内存使用大与管理困难的问题。
@@ -100,7 +112,7 @@ sing-box 1.8.0 引入了[规则集](/zh/configuration/rule-set/)
#### Geosite #### Geosite
Geosite 已废弃且在 sing-box 1.12.0 中被移除。 Geosite 已废弃且在 sing-box 1.12.0 中被移除。
Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, Geosite即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案,
存在着包括缺少维护、规则不准确和管理困难内的大量问题。 存在着包括缺少维护、规则不准确和管理困难内的大量问题。

View File

@@ -2,6 +2,83 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.14.0
### Migrate inline ACME to certificate provider
Inline ACME options in TLS are deprecated and can be replaced by certificate providers.
Most `tls.acme` fields can be moved into the ACME certificate provider unchanged.
See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly added in sing-box 1.14.0.
!!! info "References"
[TLS](/configuration/shared/tls/#certificate_provider) /
[Certificate Provider](/configuration/shared/certificate-provider/)
=== ":material-card-remove: Deprecated"
```json
{
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"acme": {
"domain": ["example.com"],
"email": "admin@example.com"
}
}
}
]
}
```
=== ":material-card-multiple: Inline"
```json
{
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"certificate_provider": {
"type": "acme",
"domain": ["example.com"],
"email": "admin@example.com"
}
}
}
]
}
```
=== ":material-card-multiple: Shared"
```json
{
"certificate_providers": [
{
"type": "acme",
"tag": "my-cert",
"domain": ["example.com"],
"email": "admin@example.com"
}
],
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"certificate_provider": "my-cert"
}
}
]
}
```
## 1.12.0 ## 1.12.0
### Migrate to new DNS server formats ### Migrate to new DNS server formats

View File

@@ -2,6 +2,83 @@
icon: material/arrange-bring-forward icon: material/arrange-bring-forward
--- ---
## 1.14.0
### 迁移内联 ACME 到证书提供者
TLS 中的内联 ACME 选项已废弃,且可以被证书提供者替代。
`tls.acme` 的大多数字段都可以原样迁移到 ACME 证书提供者中。
sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-provider/acme/) 页面。
!!! info "参考"
[TLS](/zh/configuration/shared/tls/#certificate_provider) /
[证书提供者](/zh/configuration/shared/certificate-provider/)
=== ":material-card-remove: 弃用的"
```json
{
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"acme": {
"domain": ["example.com"],
"email": "admin@example.com"
}
}
}
]
}
```
=== ":material-card-multiple: 内联"
```json
{
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"certificate_provider": {
"type": "acme",
"domain": ["example.com"],
"email": "admin@example.com"
}
}
}
]
}
```
=== ":material-card-multiple: 共享"
```json
{
"certificate_providers": [
{
"type": "acme",
"tag": "my-cert",
"domain": ["example.com"],
"email": "admin@example.com"
}
],
"inbounds": [
{
"type": "trojan",
"tls": {
"enabled": true,
"certificate_provider": "my-cert"
}
}
]
}
```
## 1.12.0 ## 1.12.0
### 迁移到新的 DNS 服务器格式 ### 迁移到新的 DNS 服务器格式

View File

@@ -102,10 +102,20 @@ var OptionLegacyDomainStrategyOptions = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-domain-strategy-options", MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-domain-strategy-options",
} }
var OptionInlineACME = Note{
Name: "inline-acme-options",
Description: "inline ACME options in TLS",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "INLINE_ACME_OPTIONS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
}
var Options = []Note{ var Options = []Note{
OptionLegacyDNSTransport, OptionLegacyDNSTransport,
OptionLegacyDNSFakeIPOptions, OptionLegacyDNSFakeIPOptions,
OptionOutboundDNSRuleItem, OptionOutboundDNSRuleItem,
OptionMissingDomainResolver, OptionMissingDomainResolver,
OptionLegacyDomainStrategyOptions, OptionLegacyDomainStrategyOptions,
OptionInlineACME,
} }

View File

@@ -33,7 +33,7 @@ func baseContext(platformInterface PlatformInterface) context.Context {
} }
ctx := context.Background() ctx := context.Background()
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry()) return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry())
} }
func parseConfig(ctx context.Context, configContent string) (option.Options, error) { func parseConfig(ctx context.Context, configContent string) (option.Options, error) {

12
include/acme.go Normal file
View File

@@ -0,0 +1,12 @@
//go:build with_acme
package include
import (
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/service/acme"
)
func registerACMECertificateProvider(registry *certificate.Registry) {
acme.RegisterCertificateProvider(registry)
}

20
include/acme_stub.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build !with_acme
package include
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func registerACMECertificateProvider(registry *certificate.Registry) {
certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) {
return nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
})
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/sagernet/sing-box" "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
@@ -34,13 +35,14 @@ import (
"github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess" "github.com/sagernet/sing-box/protocol/vmess"
originca "github.com/sagernet/sing-box/service/origin_ca"
"github.com/sagernet/sing-box/service/resolved" "github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-box/service/ssmapi" "github.com/sagernet/sing-box/service/ssmapi"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func Context(ctx context.Context) context.Context { func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry()) return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry())
} }
func InboundRegistry() *inbound.Registry { func InboundRegistry() *inbound.Registry {
@@ -139,6 +141,16 @@ func ServiceRegistry() *service.Registry {
return registry return registry
} }
func CertificateProviderRegistry() *certificate.Registry {
registry := certificate.NewRegistry()
registerACMECertificateProvider(registry)
registerTailscaleCertificateProvider(registry)
originca.RegisterCertificateProvider(registry)
return registry
}
func registerStubForRemovedInbounds(registry *inbound.Registry) { func registerStubForRemovedInbounds(registry *inbound.Registry) {
inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

@@ -3,6 +3,7 @@
package include package include
import ( import (
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
@@ -18,6 +19,10 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
tailscale.RegistryTransport(registry) tailscale.RegistryTransport(registry)
} }
func registerTailscaleCertificateProvider(registry *certificate.Registry) {
tailscale.RegisterCertificateProvider(registry)
}
func registerDERPService(registry *service.Registry) { func registerDERPService(registry *service.Registry) {
derp.Register(registry) derp.Register(registry)
} }

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
@@ -27,6 +28,12 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
}) })
} }
func registerTailscaleCertificateProvider(registry *certificate.Registry) {
certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
})
}
func registerDERPService(registry *service.Registry) { func registerDERPService(registry *service.Registry) {
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`) return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)

View File

@@ -122,6 +122,11 @@ nav:
- Listen Fields: configuration/shared/listen.md - Listen Fields: configuration/shared/listen.md
- Dial Fields: configuration/shared/dial.md - Dial Fields: configuration/shared/dial.md
- TLS: configuration/shared/tls.md - TLS: configuration/shared/tls.md
- Certificate Provider:
- configuration/shared/certificate-provider/index.md
- ACME: configuration/shared/certificate-provider/acme.md
- Tailscale: configuration/shared/certificate-provider/tailscale.md
- Cloudflare Origin CA: configuration/shared/certificate-provider/cloudflare-origin-ca.md
- DNS01 Challenge Fields: configuration/shared/dns01_challenge.md - DNS01 Challenge Fields: configuration/shared/dns01_challenge.md
- Pre-match: configuration/shared/pre-match.md - Pre-match: configuration/shared/pre-match.md
- Multiplex: configuration/shared/multiplex.md - Multiplex: configuration/shared/multiplex.md
@@ -273,6 +278,7 @@ plugins:
Shared: 通用 Shared: 通用
Listen Fields: 监听字段 Listen Fields: 监听字段
Dial Fields: 拨号字段 Dial Fields: 拨号字段
Certificate Provider Fields: 证书提供者字段
DNS01 Challenge Fields: DNS01 验证字段 DNS01 Challenge Fields: DNS01 验证字段
Multiplex: 多路复用 Multiplex: 多路复用
V2Ray Transport: V2Ray 传输层 V2Ray Transport: V2Ray 传输层
@@ -281,6 +287,7 @@ plugins:
Endpoint: 端点 Endpoint: 端点
Inbound: 入站 Inbound: 入站
Outbound: 出站 Outbound: 出站
Certificate Provider: 证书提供者
Manual: 手册 Manual: 手册
reconfigure_material: true reconfigure_material: true

106
option/acme.go Normal file
View File

@@ -0,0 +1,106 @@
package option
import (
"strings"
C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type ACMECertificateProviderOptions struct {
Domain badoption.Listable[string] `json:"domain,omitempty"`
DataDirectory string `json:"data_directory,omitempty"`
DefaultServerName string `json:"default_server_name,omitempty"`
Email string `json:"email,omitempty"`
Provider string `json:"provider,omitempty"`
AccountKey string `json:"account_key,omitempty"`
DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"`
DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"`
AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"`
AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"`
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
DNS01Challenge *ACMEProviderDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
KeyType ACMEKeyType `json:"key_type,omitempty"`
Detour string `json:"detour,omitempty"`
}
type _ACMEProviderDNS01ChallengeOptions struct {
TTL badoption.Duration `json:"ttl,omitempty"`
PropagationDelay badoption.Duration `json:"propagation_delay,omitempty"`
PropagationTimeout badoption.Duration `json:"propagation_timeout,omitempty"`
Resolvers badoption.Listable[string] `json:"resolvers,omitempty"`
OverrideDomain string `json:"override_domain,omitempty"`
Provider string `json:"provider,omitempty"`
AliDNSOptions ACMEDNS01AliDNSOptions `json:"-"`
CloudflareOptions ACMEDNS01CloudflareOptions `json:"-"`
ACMEDNSOptions ACMEDNS01ACMEDNSOptions `json:"-"`
}
type ACMEProviderDNS01ChallengeOptions _ACMEProviderDNS01ChallengeOptions
func (o ACMEProviderDNS01ChallengeOptions) MarshalJSON() ([]byte, error) {
var v any
switch o.Provider {
case C.DNSProviderAliDNS:
v = o.AliDNSOptions
case C.DNSProviderCloudflare:
v = o.CloudflareOptions
case C.DNSProviderACMEDNS:
v = o.ACMEDNSOptions
case "":
return nil, E.New("missing provider type")
default:
return nil, E.New("unknown provider type: ", o.Provider)
}
return badjson.MarshallObjects((_ACMEProviderDNS01ChallengeOptions)(o), v)
}
func (o *ACMEProviderDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o))
if err != nil {
return err
}
var v any
switch o.Provider {
case C.DNSProviderAliDNS:
v = &o.AliDNSOptions
case C.DNSProviderCloudflare:
v = &o.CloudflareOptions
case C.DNSProviderACMEDNS:
v = &o.ACMEDNSOptions
case "":
return E.New("missing provider type")
default:
return E.New("unknown provider type: ", o.Provider)
}
return badjson.UnmarshallExcluded(bytes, (*_ACMEProviderDNS01ChallengeOptions)(o), v)
}
type ACMEKeyType string
const (
ACMEKeyTypeED25519 = ACMEKeyType("ed25519")
ACMEKeyTypeP256 = ACMEKeyType("p256")
ACMEKeyTypeP384 = ACMEKeyType("p384")
ACMEKeyTypeRSA2048 = ACMEKeyType("rsa2048")
ACMEKeyTypeRSA4096 = ACMEKeyType("rsa4096")
)
func (t *ACMEKeyType) UnmarshalJSON(data []byte) error {
var value string
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
value = strings.ToLower(value)
switch ACMEKeyType(value) {
case "", ACMEKeyTypeED25519, ACMEKeyTypeP256, ACMEKeyTypeP384, ACMEKeyTypeRSA2048, ACMEKeyTypeRSA4096:
*t = ACMEKeyType(value)
default:
return E.New("unknown ACME key type: ", value)
}
return nil
}

View File

@@ -0,0 +1,100 @@
package option
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service"
)
type CertificateProviderOptionsRegistry interface {
CreateOptions(providerType string) (any, bool)
}
type _CertificateProvider struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type CertificateProvider _CertificateProvider
func (h *CertificateProvider) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_CertificateProvider)(h), h.Options)
}
func (h *CertificateProvider) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_CertificateProvider)(h))
if err != nil {
return err
}
registry := service.FromContext[CertificateProviderOptionsRegistry](ctx)
if registry == nil {
return E.New("missing certificate provider options registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown certificate provider type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_CertificateProvider)(h), options)
if err != nil {
return err
}
h.Options = options
return nil
}
type CertificateProviderOptions struct {
Tag string `json:"-"`
Type string `json:"-"`
Options any `json:"-"`
}
type _CertificateProviderInline struct {
Type string `json:"type"`
}
func (o *CertificateProviderOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
if o.Tag != "" {
return json.Marshal(o.Tag)
}
return badjson.MarshallObjectsContext(ctx, _CertificateProviderInline{Type: o.Type}, o.Options)
}
func (o *CertificateProviderOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
if len(content) == 0 {
return E.New("empty certificate_provider value")
}
if content[0] == '"' {
return json.UnmarshalContext(ctx, content, &o.Tag)
}
var inline _CertificateProviderInline
err := json.UnmarshalContext(ctx, content, &inline)
if err != nil {
return err
}
o.Type = inline.Type
if o.Type == "" {
return E.New("missing certificate provider type")
}
registry := service.FromContext[CertificateProviderOptionsRegistry](ctx)
if registry == nil {
return E.New("missing certificate provider options registry in context")
}
options, loaded := registry.CreateOptions(o.Type)
if !loaded {
return E.New("unknown certificate provider type: ", o.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, &inline, options)
if err != nil {
return err
}
o.Options = options
return nil
}
func (o *CertificateProviderOptions) IsShared() bool {
return o.Tag != ""
}

View File

@@ -10,18 +10,19 @@ import (
) )
type _Options struct { type _Options struct {
RawMessage json.RawMessage `json:"-"` RawMessage json.RawMessage `json:"-"`
Schema string `json:"$schema,omitempty"` Schema string `json:"$schema,omitempty"`
Log *LogOptions `json:"log,omitempty"` Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"` DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"` NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"` Certificate *CertificateOptions `json:"certificate,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"` CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"` Endpoints []Endpoint `json:"endpoints,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"`
Services []Service `json:"services,omitempty"` Route *RouteOptions `json:"route,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"` Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
} }
type Options _Options type Options _Options
@@ -56,6 +57,25 @@ func checkOptions(options *Options) error {
if err != nil { if err != nil {
return err return err
} }
err = checkCertificateProviders(options.CertificateProviders)
if err != nil {
return err
}
return nil
}
func checkCertificateProviders(providers []CertificateProvider) error {
seen := make(map[string]bool)
for i, provider := range providers {
tag := provider.Tag
if tag == "" {
tag = F.ToString(i)
}
if seen[tag] {
return E.New("duplicate certificate provider tag: ", tag)
}
seen[tag] = true
}
return nil return nil
} }

76
option/origin_ca.go Normal file
View File

@@ -0,0 +1,76 @@
package option
import (
"strings"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type CloudflareOriginCACertificateProviderOptions struct {
Domain badoption.Listable[string] `json:"domain,omitempty"`
DataDirectory string `json:"data_directory,omitempty"`
APIToken string `json:"api_token,omitempty"`
OriginCAKey string `json:"origin_ca_key,omitempty"`
RequestType CloudflareOriginCARequestType `json:"request_type,omitempty"`
RequestedValidity CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
Detour string `json:"detour,omitempty"`
}
type CloudflareOriginCARequestType string
const (
CloudflareOriginCARequestTypeOriginRSA = CloudflareOriginCARequestType("origin-rsa")
CloudflareOriginCARequestTypeOriginECC = CloudflareOriginCARequestType("origin-ecc")
)
func (t *CloudflareOriginCARequestType) UnmarshalJSON(data []byte) error {
var value string
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
value = strings.ToLower(value)
switch CloudflareOriginCARequestType(value) {
case "", CloudflareOriginCARequestTypeOriginRSA, CloudflareOriginCARequestTypeOriginECC:
*t = CloudflareOriginCARequestType(value)
default:
return E.New("unsupported Cloudflare Origin CA request type: ", value)
}
return nil
}
type CloudflareOriginCARequestValidity uint16
const (
CloudflareOriginCARequestValidity7 = CloudflareOriginCARequestValidity(7)
CloudflareOriginCARequestValidity30 = CloudflareOriginCARequestValidity(30)
CloudflareOriginCARequestValidity90 = CloudflareOriginCARequestValidity(90)
CloudflareOriginCARequestValidity365 = CloudflareOriginCARequestValidity(365)
CloudflareOriginCARequestValidity730 = CloudflareOriginCARequestValidity(730)
CloudflareOriginCARequestValidity1095 = CloudflareOriginCARequestValidity(1095)
CloudflareOriginCARequestValidity5475 = CloudflareOriginCARequestValidity(5475)
)
func (v *CloudflareOriginCARequestValidity) UnmarshalJSON(data []byte) error {
var value uint16
err := json.Unmarshal(data, &value)
if err != nil {
return err
}
switch CloudflareOriginCARequestValidity(value) {
case 0,
CloudflareOriginCARequestValidity7,
CloudflareOriginCARequestValidity30,
CloudflareOriginCARequestValidity90,
CloudflareOriginCARequestValidity365,
CloudflareOriginCARequestValidity730,
CloudflareOriginCARequestValidity1095,
CloudflareOriginCARequestValidity5475:
*v = CloudflareOriginCARequestValidity(value)
default:
return E.New("unsupported Cloudflare Origin CA requested validity: ", value)
}
return nil
}

View File

@@ -36,6 +36,10 @@ type TailscaleDNSServerOptions struct {
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
} }
type TailscaleCertificateProviderOptions struct {
Endpoint string `json:"endpoint,omitempty"`
}
type DERPServiceOptions struct { type DERPServiceOptions struct {
ListenOptions ListenOptions
InboundTLSOptionsContainer InboundTLSOptionsContainer

View File

@@ -28,9 +28,13 @@ type InboundTLSOptions struct {
KeyPath string `json:"key_path,omitempty"` KeyPath string `json:"key_path,omitempty"`
KernelTx bool `json:"kernel_tx,omitempty"` KernelTx bool `json:"kernel_tx,omitempty"`
KernelRx bool `json:"kernel_rx,omitempty"` KernelRx bool `json:"kernel_rx,omitempty"`
ACME *InboundACMEOptions `json:"acme,omitempty"` CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"`
ECH *InboundECHOptions `json:"ech,omitempty"`
Reality *InboundRealityOptions `json:"reality,omitempty"` // Deprecated: use certificate_provider
ACME *InboundACMEOptions `json:"acme,omitempty"`
ECH *InboundECHOptions `json:"ech,omitempty"`
Reality *InboundRealityOptions `json:"reality,omitempty"`
} }
type ClientAuthType tls.ClientAuthType type ClientAuthType tls.ClientAuthType

View File

@@ -0,0 +1,98 @@
//go:build with_gvisor
package tailscale
import (
"context"
"crypto/tls"
"net"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/tailscale/client/local"
)
func RegisterCertificateProvider(registry *certificate.Registry) {
certificate.Register[option.TailscaleCertificateProviderOptions](registry, C.TypeTailscale, NewCertificateProvider)
}
var _ adapter.CertificateProviderService = (*CertificateProvider)(nil)
type CertificateProvider struct {
certificate.Adapter
endpointTag string
endpoint *Endpoint
dialer N.Dialer
localClient *local.Client
}
func NewCertificateProvider(ctx context.Context, _ log.ContextLogger, tag string, options option.TailscaleCertificateProviderOptions) (adapter.CertificateProviderService, error) {
if options.Endpoint == "" {
return nil, E.New("missing tailscale endpoint tag")
}
endpointManager := service.FromContext[adapter.EndpointManager](ctx)
if endpointManager == nil {
return nil, E.New("missing endpoint manager in context")
}
rawEndpoint, loaded := endpointManager.Get(options.Endpoint)
if !loaded {
return nil, E.New("endpoint not found: ", options.Endpoint)
}
endpoint, isTailscale := rawEndpoint.(*Endpoint)
if !isTailscale {
return nil, E.New("endpoint is not Tailscale: ", options.Endpoint)
}
providerDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: option.DialerOptions{},
RemoteIsDomain: true,
})
if err != nil {
return nil, E.Cause(err, "create tailscale certificate provider dialer")
}
return &CertificateProvider{
Adapter: certificate.NewAdapter(C.TypeTailscale, tag),
endpointTag: options.Endpoint,
endpoint: endpoint,
dialer: providerDialer,
}, nil
}
func (p *CertificateProvider) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
localClient, err := p.endpoint.Server().LocalClient()
if err != nil {
return E.Cause(err, "initialize tailscale local client for endpoint ", p.endpointTag)
}
originalDial := localClient.Dial
localClient.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
if originalDial != nil && addr == "local-tailscaled.sock:80" {
return originalDial(ctx, network, addr)
}
return p.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
p.localClient = localClient
return nil
}
func (p *CertificateProvider) Close() error {
return nil
}
func (p *CertificateProvider) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
localClient := p.localClient
if localClient == nil {
return nil, E.New("Tailscale is not ready yet")
}
return localClient.GetCertificate(clientHello)
}

411
service/acme/service.go Normal file
View File

@@ -0,0 +1,411 @@
//go:build with_acme
package acme
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"net"
"net/http"
"net/url"
"reflect"
"strings"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/common/dialer"
boxtls "github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/ntp"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/zerossl"
"github.com/libdns/alidns"
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
"github.com/mholt/acmez/v3/acme"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func RegisterCertificateProvider(registry *certificate.Registry) {
certificate.Register[option.ACMECertificateProviderOptions](registry, C.TypeACME, NewCertificateProvider)
}
var (
_ adapter.CertificateProviderService = (*Service)(nil)
_ adapter.ACMECertificateProvider = (*Service)(nil)
)
type Service struct {
certificate.Adapter
ctx context.Context
config *certmagic.Config
cache *certmagic.Cache
domain []string
nextProtos []string
}
func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMECertificateProviderOptions) (adapter.CertificateProviderService, error) {
if len(options.Domain) == 0 {
return nil, E.New("missing domain")
}
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, E.New("unsupported ACME provider: ", options.Provider)
}
acmeServer = options.Provider
}
if acmeServer == certmagic.ZeroSSLProductionCA &&
(options.ExternalAccount == nil || options.ExternalAccount.KeyID == "") &&
strings.TrimSpace(options.Email) == "" &&
strings.TrimSpace(options.AccountKey) == "" {
return nil, E.New("email is required to use the ZeroSSL ACME endpoint without external_account or account_key")
}
var storage certmagic.Storage
if options.DataDirectory != "" {
storage = &certmagic.FileStorage{Path: options.DataDirectory}
} else {
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(boxtls.ACMEEncoderConfig()),
&boxtls.ACMELogWriter{Logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
DefaultServerName: options.DefaultServerName,
Storage: storage,
Logger: zapLogger,
}
if options.KeyType != "" {
var keyType certmagic.KeyType
switch options.KeyType {
case option.ACMEKeyTypeED25519:
keyType = certmagic.ED25519
case option.ACMEKeyTypeP256:
keyType = certmagic.P256
case option.ACMEKeyTypeP384:
keyType = certmagic.P384
case option.ACMEKeyTypeRSA2048:
keyType = certmagic.RSA2048
case option.ACMEKeyTypeRSA4096:
keyType = certmagic.RSA4096
default:
return nil, E.New("unsupported ACME key type: ", options.KeyType)
}
config.KeySource = certmagic.StandardKeyGenerator{KeyType: keyType}
}
acmeIssuer := certmagic.ACMEIssuer{
CA: acmeServer,
Email: options.Email,
AccountKeyPEM: options.AccountKey,
Agreed: true,
DisableHTTPChallenge: options.DisableHTTPChallenge,
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
AltHTTPPort: int(options.AlternativeHTTPPort),
AltTLSALPNPort: int(options.AlternativeTLSPort),
Logger: zapLogger,
}
acmeHTTPClient, err := newACMEHTTPClient(ctx, options.Detour)
if err != nil {
return nil, err
}
dnsSolver, err := newDNSSolver(options.DNS01Challenge, zapLogger, acmeHTTPClient)
if err != nil {
return nil, err
}
if dnsSolver != nil {
acmeIssuer.DNS01Solver = dnsSolver
}
if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
acmeIssuer.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
}
if acmeServer == certmagic.ZeroSSLProductionCA {
acmeIssuer.NewAccountFunc = func(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account) (acme.Account, error) {
if acmeIssuer.ExternalAccount != nil {
return account, nil
}
var err error
acmeIssuer.ExternalAccount, account, err = createZeroSSLExternalAccountBinding(ctx, acmeIssuer, account, acmeHTTPClient)
return account, err
}
}
certmagicIssuer := certmagic.NewACMEIssuer(config, acmeIssuer)
httpClientField := reflect.ValueOf(certmagicIssuer).Elem().FieldByName("httpClient")
if !httpClientField.IsValid() || !httpClientField.CanAddr() {
return nil, E.New("certmagic ACME issuer HTTP client field is unavailable")
}
reflect.NewAt(httpClientField.Type(), unsafe.Pointer(httpClientField.UnsafeAddr())).Elem().Set(reflect.ValueOf(acmeHTTPClient))
config.Issuers = []certmagic.Issuer{certmagicIssuer}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
return config, nil
},
Logger: zapLogger,
})
config = certmagic.New(cache, *config)
var nextProtos []string
if !acmeIssuer.DisableTLSALPNChallenge && acmeIssuer.DNS01Solver == nil {
nextProtos = []string{C.ACMETLS1Protocol}
}
return &Service{
Adapter: certificate.NewAdapter(C.TypeACME, tag),
ctx: ctx,
config: config,
cache: cache,
domain: options.Domain,
nextProtos: nextProtos,
}, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return s.config.ManageAsync(s.ctx, s.domain)
}
func (s *Service) Close() error {
if s.cache != nil {
s.cache.Stop()
}
return nil
}
func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return s.config.GetCertificate(hello)
}
func (s *Service) GetACMENextProtos() []string {
return s.nextProtos
}
func newDNSSolver(dnsOptions *option.ACMEProviderDNS01ChallengeOptions, logger *zap.Logger, httpClient *http.Client) (*certmagic.DNS01Solver, error) {
if dnsOptions == nil || dnsOptions.Provider == "" {
return nil, nil
}
if dnsOptions.TTL < 0 {
return nil, E.New("invalid ACME DNS01 ttl: ", dnsOptions.TTL)
}
if dnsOptions.PropagationDelay < 0 {
return nil, E.New("invalid ACME DNS01 propagation_delay: ", dnsOptions.PropagationDelay)
}
if dnsOptions.PropagationTimeout < -1 {
return nil, E.New("invalid ACME DNS01 propagation_timeout: ", dnsOptions.PropagationTimeout)
}
solver := &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{
TTL: time.Duration(dnsOptions.TTL),
PropagationDelay: time.Duration(dnsOptions.PropagationDelay),
PropagationTimeout: time.Duration(dnsOptions.PropagationTimeout),
Resolvers: dnsOptions.Resolvers,
OverrideDomain: dnsOptions.OverrideDomain,
Logger: logger.Named("dns_manager"),
},
}
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,
HTTPClient: httpClient,
}
case C.DNSProviderACMEDNS:
solver.DNSProvider = &acmeDNSProvider{
username: dnsOptions.ACMEDNSOptions.Username,
password: dnsOptions.ACMEDNSOptions.Password,
subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
serverURL: dnsOptions.ACMEDNSOptions.ServerURL,
httpClient: httpClient,
}
default:
return nil, E.New("unsupported ACME DNS01 provider type: ", dnsOptions.Provider)
}
return solver, nil
}
func createZeroSSLExternalAccountBinding(ctx context.Context, acmeIssuer *certmagic.ACMEIssuer, account acme.Account, httpClient *http.Client) (*acme.EAB, acme.Account, error) {
email := strings.TrimSpace(acmeIssuer.Email)
if email == "" {
return nil, acme.Account{}, E.New("email is required to use the ZeroSSL ACME endpoint without external_account")
}
if len(account.Contact) == 0 {
account.Contact = []string{"mailto:" + email}
}
if acmeIssuer.CertObtainTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, acmeIssuer.CertObtainTimeout)
defer cancel()
}
form := url.Values{"email": []string{email}}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, zerossl.BaseURL+"/acme/eab-credentials-email", strings.NewReader(form.Encode()))
if err != nil {
return nil, account, E.Cause(err, "create ZeroSSL EAB request")
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("User-Agent", certmagic.UserAgent)
response, err := httpClient.Do(request)
if err != nil {
return nil, account, E.Cause(err, "request ZeroSSL EAB")
}
defer response.Body.Close()
var result struct {
Success bool `json:"success"`
Error struct {
Code int `json:"code"`
Type string `json:"type"`
} `json:"error"`
EABKID string `json:"eab_kid"`
EABHMACKey string `json:"eab_hmac_key"`
}
err = json.NewDecoder(response.Body).Decode(&result)
if err != nil {
return nil, account, E.Cause(err, "decode ZeroSSL EAB response")
}
if response.StatusCode != http.StatusOK {
return nil, account, E.New("failed getting ZeroSSL EAB credentials: HTTP ", response.StatusCode)
}
if result.Error.Code != 0 {
return nil, account, E.New("failed getting ZeroSSL EAB credentials: ", result.Error.Type, " (code ", result.Error.Code, ")")
}
acmeIssuer.Logger.Info("generated ZeroSSL EAB credentials", zap.String("key_id", result.EABKID))
return &acme.EAB{
KeyID: result.EABKID,
MACKey: result.EABHMACKey,
}, account, nil
}
func newACMEHTTPClient(ctx context.Context, detour string) (*http.Client, error) {
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: option.DialerOptions{
Detour: detour,
},
RemoteIsDomain: true,
})
if err != nil {
return nil, E.Cause(err, "create ACME provider dialer")
}
return &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(ctx),
Time: ntp.TimeFuncFromContext(ctx),
},
// from certmagic defaults (acmeissuer.go)
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 2 * time.Second,
ForceAttemptHTTP2: true,
},
Timeout: certmagic.HTTPTimeout,
}, nil
}
type acmeDNSProvider struct {
username string
password string
subdomain string
serverURL string
httpClient *http.Client
}
type acmeDNSRecord struct {
resourceRecord libdns.RR
}
func (r acmeDNSRecord) RR() libdns.RR {
return r.resourceRecord
}
func (p *acmeDNSProvider) AppendRecords(ctx context.Context, _ string, records []libdns.Record) ([]libdns.Record, error) {
if p.username == "" {
return nil, E.New("ACME-DNS username cannot be empty")
}
if p.password == "" {
return nil, E.New("ACME-DNS password cannot be empty")
}
if p.subdomain == "" {
return nil, E.New("ACME-DNS subdomain cannot be empty")
}
if p.serverURL == "" {
return nil, E.New("ACME-DNS server_url cannot be empty")
}
appendedRecords := make([]libdns.Record, 0, len(records))
for _, record := range records {
resourceRecord := record.RR()
if resourceRecord.Type != "TXT" {
return appendedRecords, E.New("ACME-DNS only supports adding TXT records")
}
requestBody, err := json.Marshal(map[string]string{
"subdomain": p.subdomain,
"txt": resourceRecord.Data,
})
if err != nil {
return appendedRecords, E.Cause(err, "marshal ACME-DNS update request")
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, p.serverURL+"/update", bytes.NewReader(requestBody))
if err != nil {
return appendedRecords, E.Cause(err, "create ACME-DNS update request")
}
request.Header.Set("X-Api-User", p.username)
request.Header.Set("X-Api-Key", p.password)
request.Header.Set("Content-Type", "application/json")
response, err := p.httpClient.Do(request)
if err != nil {
return appendedRecords, E.Cause(err, "update ACME-DNS record")
}
_ = response.Body.Close()
if response.StatusCode != http.StatusOK {
return appendedRecords, E.New("update ACME-DNS record: HTTP ", response.StatusCode)
}
appendedRecords = append(appendedRecords, acmeDNSRecord{resourceRecord: libdns.RR{
Type: "TXT",
Name: resourceRecord.Name,
Data: resourceRecord.Data,
}})
}
return appendedRecords, nil
}
func (p *acmeDNSProvider) DeleteRecords(context.Context, string, []libdns.Record) ([]libdns.Record, error) {
return nil, nil
}

3
service/acme/stub.go Normal file
View File

@@ -0,0 +1,3 @@
//go:build !with_acme
package acme

View File

@@ -0,0 +1,618 @@
package originca
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"io"
"io/fs"
"net"
"net/http"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/ntp"
"github.com/caddyserver/certmagic"
)
const (
cloudflareOriginCAEndpoint = "https://api.cloudflare.com/client/v4/certificates"
defaultRequestedValidity = option.CloudflareOriginCARequestValidity5475
// min of 30 days and certmagic's 1/3 lifetime ratio (maintain.go)
defaultRenewBefore = 30 * 24 * time.Hour
// from certmagic retry backoff range (async.go)
minimumRenewRetryDelay = time.Minute
maximumRenewRetryDelay = time.Hour
storageLockPrefix = "cloudflare-origin-ca"
)
func RegisterCertificateProvider(registry *certificate.Registry) {
certificate.Register[option.CloudflareOriginCACertificateProviderOptions](registry, C.TypeCloudflareOriginCA, NewCertificateProvider)
}
var _ adapter.CertificateProviderService = (*Service)(nil)
type Service struct {
certificate.Adapter
logger log.ContextLogger
ctx context.Context
cancel context.CancelFunc
done chan struct{}
timeFunc func() time.Time
httpClient *http.Client
storage certmagic.Storage
storageIssuerKey string
storageNamesKey string
storageLockKey string
apiToken string
originCAKey string
domain []string
requestType option.CloudflareOriginCARequestType
requestedValidity option.CloudflareOriginCARequestValidity
access sync.RWMutex
currentCertificate *tls.Certificate
currentLeaf *x509.Certificate
}
func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.CloudflareOriginCACertificateProviderOptions) (adapter.CertificateProviderService, error) {
domain, err := normalizeHostnames(options.Domain)
if err != nil {
return nil, err
}
if len(domain) == 0 {
return nil, E.New("missing domain")
}
apiToken := strings.TrimSpace(options.APIToken)
originCAKey := strings.TrimSpace(options.OriginCAKey)
switch {
case apiToken == "" && originCAKey == "":
return nil, E.New("api_token or origin_ca_key is required")
case apiToken != "" && originCAKey != "":
return nil, E.New("api_token and origin_ca_key are mutually exclusive")
}
requestType := options.RequestType
if requestType == "" {
requestType = option.CloudflareOriginCARequestTypeOriginRSA
}
requestedValidity := options.RequestedValidity
if requestedValidity == 0 {
requestedValidity = defaultRequestedValidity
}
ctx, cancel := context.WithCancel(ctx)
serviceDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: option.DialerOptions{
Detour: options.Detour,
},
RemoteIsDomain: true,
})
if err != nil {
cancel()
return nil, E.Cause(err, "create Cloudflare Origin CA dialer")
}
var storage certmagic.Storage
if options.DataDirectory != "" {
storage = &certmagic.FileStorage{Path: options.DataDirectory}
} else {
storage = certmagic.Default.Storage
}
timeFunc := ntp.TimeFuncFromContext(ctx)
if timeFunc == nil {
timeFunc = time.Now
}
storageIssuerKey := C.TypeCloudflareOriginCA + "-" + string(requestType)
storageNamesKey := (&certmagic.CertificateResource{SANs: slices.Clone(domain)}).NamesKey()
storageLockKey := strings.Join([]string{
storageLockPrefix,
certmagic.StorageKeys.Safe(storageIssuerKey),
certmagic.StorageKeys.Safe(storageNamesKey),
}, "/")
return &Service{
Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag),
logger: logger,
ctx: ctx,
cancel: cancel,
timeFunc: timeFunc,
httpClient: &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(ctx),
Time: timeFunc,
},
ForceAttemptHTTP2: true,
}},
storage: storage,
storageIssuerKey: storageIssuerKey,
storageNamesKey: storageNamesKey,
storageLockKey: storageLockKey,
apiToken: apiToken,
originCAKey: originCAKey,
domain: domain,
requestType: requestType,
requestedValidity: requestedValidity,
}, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
cachedCertificate, cachedLeaf, err := s.loadCachedCertificate()
if err != nil {
s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate"))
} else if cachedCertificate != nil {
s.setCurrentCertificate(cachedCertificate, cachedLeaf)
}
if cachedCertificate == nil {
err = s.issueAndStoreCertificate()
if err != nil {
return err
}
} else if s.shouldRenew(cachedLeaf, s.timeFunc()) {
err = s.issueAndStoreCertificate()
if err != nil {
s.logger.Warn(E.Cause(err, "renew cached Cloudflare Origin CA certificate"))
}
}
s.done = make(chan struct{})
go s.refreshLoop()
return nil
}
func (s *Service) Close() error {
s.cancel()
if done := s.done; done != nil {
<-done
}
if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded {
transport.CloseIdleConnections()
}
return nil
}
func (s *Service) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
s.access.RLock()
certificate := s.currentCertificate
s.access.RUnlock()
if certificate == nil {
return nil, E.New("Cloudflare Origin CA certificate is unavailable")
}
return certificate, nil
}
func (s *Service) refreshLoop() {
defer close(s.done)
var retryDelay time.Duration
for {
waitDuration := retryDelay
if waitDuration == 0 {
s.access.RLock()
leaf := s.currentLeaf
s.access.RUnlock()
if leaf == nil {
waitDuration = minimumRenewRetryDelay
} else {
refreshAt := leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf))
waitDuration = refreshAt.Sub(s.timeFunc())
if waitDuration < minimumRenewRetryDelay {
waitDuration = minimumRenewRetryDelay
}
}
}
timer := time.NewTimer(waitDuration)
select {
case <-s.ctx.Done():
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
return
case <-timer.C:
}
err := s.issueAndStoreCertificate()
if err != nil {
s.logger.Error(E.Cause(err, "renew Cloudflare Origin CA certificate"))
s.access.RLock()
leaf := s.currentLeaf
s.access.RUnlock()
if leaf == nil {
retryDelay = minimumRenewRetryDelay
} else {
remaining := leaf.NotAfter.Sub(s.timeFunc())
switch {
case remaining <= minimumRenewRetryDelay:
retryDelay = minimumRenewRetryDelay
case remaining < maximumRenewRetryDelay:
retryDelay = max(remaining/2, minimumRenewRetryDelay)
default:
retryDelay = maximumRenewRetryDelay
}
}
continue
}
retryDelay = 0
}
}
func (s *Service) shouldRenew(leaf *x509.Certificate, now time.Time) bool {
return !now.Before(leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf)))
}
func (s *Service) effectiveRenewBefore(leaf *x509.Certificate) time.Duration {
lifetime := leaf.NotAfter.Sub(leaf.NotBefore)
if lifetime <= 0 {
return 0
}
return min(lifetime/3, defaultRenewBefore)
}
func (s *Service) issueAndStoreCertificate() error {
err := s.storage.Lock(s.ctx, s.storageLockKey)
if err != nil {
return E.Cause(err, "lock Cloudflare Origin CA certificate storage")
}
defer func() {
err = s.storage.Unlock(context.WithoutCancel(s.ctx), s.storageLockKey)
if err != nil {
s.logger.Warn(E.Cause(err, "unlock Cloudflare Origin CA certificate storage"))
}
}()
cachedCertificate, cachedLeaf, err := s.loadCachedCertificate()
if err != nil {
s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate"))
} else if cachedCertificate != nil && !s.shouldRenew(cachedLeaf, s.timeFunc()) {
s.setCurrentCertificate(cachedCertificate, cachedLeaf)
return nil
}
certificatePEM, privateKeyPEM, tlsCertificate, leaf, err := s.requestCertificate(s.ctx)
if err != nil {
return err
}
issuerData, err := json.Marshal(originCAIssuerData{
RequestType: s.requestType,
RequestedValidity: s.requestedValidity,
})
if err != nil {
return E.Cause(err, "encode Cloudflare Origin CA certificate metadata")
}
err = storeCertificateResource(s.ctx, s.storage, s.storageIssuerKey, certmagic.CertificateResource{
SANs: slices.Clone(s.domain),
CertificatePEM: certificatePEM,
PrivateKeyPEM: privateKeyPEM,
IssuerData: issuerData,
})
if err != nil {
return E.Cause(err, "store Cloudflare Origin CA certificate")
}
s.setCurrentCertificate(tlsCertificate, leaf)
s.logger.Info("updated Cloudflare Origin CA certificate, expires at ", leaf.NotAfter.Format(time.RFC3339))
return nil
}
func (s *Service) requestCertificate(ctx context.Context) ([]byte, []byte, *tls.Certificate, *x509.Certificate, error) {
var privateKey crypto.Signer
switch s.requestType {
case option.CloudflareOriginCARequestTypeOriginRSA:
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, nil, nil, err
}
privateKey = rsaKey
case option.CloudflareOriginCARequestTypeOriginECC:
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, nil, nil, err
}
privateKey = ecKey
default:
return nil, nil, nil, nil, E.New("unsupported Cloudflare Origin CA request type: ", s.requestType)
}
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "encode private key")
}
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privateKeyDER,
})
certificateRequestDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
Subject: pkix.Name{CommonName: s.domain[0]},
DNSNames: s.domain,
}, privateKey)
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "create certificate request")
}
certificateRequestPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: certificateRequestDER,
})
requestBody, err := json.Marshal(originCARequest{
CSR: string(certificateRequestPEM),
Hostnames: s.domain,
RequestType: string(s.requestType),
RequestedValidity: uint16(s.requestedValidity),
})
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "marshal request")
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, cloudflareOriginCAEndpoint, bytes.NewReader(requestBody))
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "create request")
}
request.Header.Set("Accept", "application/json")
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "sing-box/"+C.Version)
if s.apiToken != "" {
request.Header.Set("Authorization", "Bearer "+s.apiToken)
} else {
request.Header.Set("X-Auth-User-Service-Key", s.originCAKey)
}
response, err := s.httpClient.Do(request)
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "request certificate from Cloudflare")
}
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "read Cloudflare response")
}
var responseEnvelope originCAResponse
err = json.Unmarshal(responseBody, &responseEnvelope)
if err != nil && response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices {
return nil, nil, nil, nil, E.Cause(err, "decode Cloudflare response")
}
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody)
}
if !responseEnvelope.Success {
return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody)
}
if responseEnvelope.Result.Certificate == "" {
return nil, nil, nil, nil, E.New("Cloudflare Origin CA response is missing certificate data")
}
certificatePEM := []byte(responseEnvelope.Result.Certificate)
tlsCertificate, leaf, err := parseKeyPair(certificatePEM, privateKeyPEM)
if err != nil {
return nil, nil, nil, nil, E.Cause(err, "parse issued certificate")
}
if !s.matchesCertificate(leaf) {
return nil, nil, nil, nil, E.New("issued Cloudflare Origin CA certificate does not match requested hostnames or key type")
}
return certificatePEM, privateKeyPEM, tlsCertificate, leaf, nil
}
func (s *Service) loadCachedCertificate() (*tls.Certificate, *x509.Certificate, error) {
certificateResource, err := loadCertificateResource(s.ctx, s.storage, s.storageIssuerKey, s.storageNamesKey)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, nil
}
return nil, nil, err
}
tlsCertificate, leaf, err := parseKeyPair(certificateResource.CertificatePEM, certificateResource.PrivateKeyPEM)
if err != nil {
return nil, nil, E.Cause(err, "parse cached key pair")
}
if s.timeFunc().After(leaf.NotAfter) {
return nil, nil, nil
}
if !s.matchesCertificate(leaf) {
return nil, nil, nil
}
return tlsCertificate, leaf, nil
}
func (s *Service) matchesCertificate(leaf *x509.Certificate) bool {
if leaf == nil {
return false
}
leafHostnames := leaf.DNSNames
if len(leafHostnames) == 0 && leaf.Subject.CommonName != "" {
leafHostnames = []string{leaf.Subject.CommonName}
}
normalizedLeafHostnames, err := normalizeHostnames(leafHostnames)
if err != nil {
return false
}
if !slices.Equal(normalizedLeafHostnames, s.domain) {
return false
}
switch s.requestType {
case option.CloudflareOriginCARequestTypeOriginRSA:
return leaf.PublicKeyAlgorithm == x509.RSA
case option.CloudflareOriginCARequestTypeOriginECC:
return leaf.PublicKeyAlgorithm == x509.ECDSA
default:
return false
}
}
func (s *Service) setCurrentCertificate(certificate *tls.Certificate, leaf *x509.Certificate) {
s.access.Lock()
s.currentCertificate = certificate
s.currentLeaf = leaf
s.access.Unlock()
}
func normalizeHostnames(hostnames []string) ([]string, error) {
normalizedHostnames := make([]string, 0, len(hostnames))
seen := make(map[string]struct{}, len(hostnames))
for _, hostname := range hostnames {
normalizedHostname := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(hostname, ".")))
if normalizedHostname == "" {
return nil, E.New("hostname is empty")
}
if net.ParseIP(normalizedHostname) != nil {
return nil, E.New("hostname cannot be an IP address: ", normalizedHostname)
}
if strings.Contains(normalizedHostname, "*") {
if !strings.HasPrefix(normalizedHostname, "*.") || strings.Count(normalizedHostname, "*") != 1 {
return nil, E.New("invalid wildcard hostname: ", normalizedHostname)
}
suffix := strings.TrimPrefix(normalizedHostname, "*.")
if strings.Count(suffix, ".") == 0 {
return nil, E.New("wildcard hostname must cover a multi-label domain: ", normalizedHostname)
}
normalizedHostname = "*." + suffix
}
if _, loaded := seen[normalizedHostname]; loaded {
continue
}
seen[normalizedHostname] = struct{}{}
normalizedHostnames = append(normalizedHostnames, normalizedHostname)
}
slices.Sort(normalizedHostnames)
return normalizedHostnames, nil
}
func parseKeyPair(certificatePEM []byte, privateKeyPEM []byte) (*tls.Certificate, *x509.Certificate, error) {
keyPair, err := tls.X509KeyPair(certificatePEM, privateKeyPEM)
if err != nil {
return nil, nil, err
}
if len(keyPair.Certificate) == 0 {
return nil, nil, E.New("certificate chain is empty")
}
leaf, err := x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return nil, nil, err
}
keyPair.Leaf = leaf
return &keyPair, leaf, nil
}
func storeCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, certificateResource certmagic.CertificateResource) error {
metaBytes, err := json.MarshalIndent(certificateResource, "", "\t")
if err != nil {
return err
}
namesKey := certificateResource.NamesKey()
keyValueList := []struct {
key string
value []byte
}{
{
key: certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey),
value: certificateResource.PrivateKeyPEM,
},
{
key: certmagic.StorageKeys.SiteCert(issuerKey, namesKey),
value: certificateResource.CertificatePEM,
},
{
key: certmagic.StorageKeys.SiteMeta(issuerKey, namesKey),
value: metaBytes,
},
}
for i, item := range keyValueList {
err = storage.Store(ctx, item.key, item.value)
if err != nil {
for j := i - 1; j >= 0; j-- {
storage.Delete(ctx, keyValueList[j].key)
}
return err
}
}
return nil
}
func loadCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, namesKey string) (certmagic.CertificateResource, error) {
privateKeyPEM, err := storage.Load(ctx, certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey))
if err != nil {
return certmagic.CertificateResource{}, err
}
certificatePEM, err := storage.Load(ctx, certmagic.StorageKeys.SiteCert(issuerKey, namesKey))
if err != nil {
return certmagic.CertificateResource{}, err
}
metaBytes, err := storage.Load(ctx, certmagic.StorageKeys.SiteMeta(issuerKey, namesKey))
if err != nil {
return certmagic.CertificateResource{}, err
}
var certificateResource certmagic.CertificateResource
err = json.Unmarshal(metaBytes, &certificateResource)
if err != nil {
return certmagic.CertificateResource{}, E.Cause(err, "decode Cloudflare Origin CA certificate metadata")
}
certificateResource.PrivateKeyPEM = privateKeyPEM
certificateResource.CertificatePEM = certificatePEM
return certificateResource, nil
}
func buildOriginCAError(statusCode int, responseErrors []originCAResponseError, responseBody []byte) error {
if len(responseErrors) > 0 {
messageList := make([]string, 0, len(responseErrors))
for _, responseError := range responseErrors {
if responseError.Message == "" {
continue
}
if responseError.Code != 0 {
messageList = append(messageList, responseError.Message+" (code "+strconv.Itoa(responseError.Code)+")")
} else {
messageList = append(messageList, responseError.Message)
}
}
if len(messageList) > 0 {
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", strings.Join(messageList, ", "))
}
}
responseText := strings.TrimSpace(string(responseBody))
if responseText == "" {
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode)
}
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", responseText)
}
type originCARequest struct {
CSR string `json:"csr"`
Hostnames []string `json:"hostnames"`
RequestType string `json:"request_type"`
RequestedValidity uint16 `json:"requested_validity"`
}
type originCAResponse struct {
Success bool `json:"success"`
Errors []originCAResponseError `json:"errors"`
Result originCAResponseResult `json:"result"`
}
type originCAResponseError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type originCAResponseResult struct {
Certificate string `json:"certificate"`
}
type originCAIssuerData struct {
RequestType option.CloudflareOriginCARequestType `json:"request_type,omitempty"`
RequestedValidity option.CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
}