mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
Refactor ACME support to certificate provider
This commit is contained in:
21
adapter/certificate/adapter.go
Normal file
21
adapter/certificate/adapter.go
Normal 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
|
||||||
|
}
|
||||||
158
adapter/certificate/manager.go
Normal file
158
adapter/certificate/manager.go
Normal 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
|
||||||
|
}
|
||||||
72
adapter/certificate/registry.go
Normal file
72
adapter/certificate/registry.go
Normal 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
|
||||||
|
}
|
||||||
38
adapter/certificate_provider.go
Normal file
38
adapter/certificate_provider.go
Normal 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
123
box.go
@@ -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},
|
||||||
|
|||||||
@@ -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
41
common/tls/acme_logger.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package tls
|
package constant
|
||||||
|
|
||||||
const ACMETLS1Protocol = "acme-tls/1"
|
const ACMETLS1Protocol = "acme-tls/1"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
150
docs/configuration/shared/certificate-provider/acme.md
Normal file
150
docs/configuration/shared/certificate-provider/acme.md
Normal 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.
|
||||||
145
docs/configuration/shared/certificate-provider/acme.zh.md
Normal file
145
docs/configuration/shared/certificate-provider/acme.zh.md
Normal 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 请求将使用此出站。
|
||||||
@@ -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.
|
||||||
@@ -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 请求将使用此出站。
|
||||||
32
docs/configuration/shared/certificate-provider/index.md
Normal file
32
docs/configuration/shared/certificate-provider/index.md
Normal 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.
|
||||||
32
docs/configuration/shared/certificate-provider/index.zh.md
Normal file
32
docs/configuration/shared/certificate-provider/index.zh.md
Normal 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
|
||||||
|
|
||||||
|
证书提供者的标签。
|
||||||
27
docs/configuration/shared/certificate-provider/tailscale.md
Normal file
27
docs/configuration/shared/certificate-provider/tailscale.md
Normal 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.
|
||||||
@@ -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)。
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
域名列表。
|
域名列表。
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 项目,作为早期流量绕过解决方案,
|
||||||
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
|
存在着包括缺少维护、规则不准确和管理困难内的大量问题。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 服务器格式
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
12
include/acme.go
Normal 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
20
include/acme_stub.go
Normal 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`)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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
106
option/acme.go
Normal 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
|
||||||
|
}
|
||||||
100
option/certificate_provider.go
Normal file
100
option/certificate_provider.go
Normal 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 != ""
|
||||||
|
}
|
||||||
@@ -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
76
option/origin_ca.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
98
protocol/tailscale/certificate_provider.go
Normal file
98
protocol/tailscale/certificate_provider.go
Normal 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
411
service/acme/service.go
Normal 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
3
service/acme/stub.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//go:build !with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
618
service/origin_ca/service.go
Normal file
618
service/origin_ca/service.go
Normal 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"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user