mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 12:48:28 +10:00
Compare commits
1 Commits
renovate/g
...
v1.14.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
734f3c9a21 |
14
adapter/certificate_provider.go
Normal file
14
adapter/certificate_provider.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateProvider interface {
|
||||||
|
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ACMECertificateProvider interface {
|
||||||
|
CertificateProvider
|
||||||
|
GetACMENextProtos() []string
|
||||||
|
}
|
||||||
36
box.go
36
box.go
@@ -272,6 +272,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 != "" {
|
||||||
@@ -298,24 +316,6 @@ 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 {
|
|
||||||
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, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
return direct.NewOutbound(
|
return direct.NewOutbound(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
const ACMETLS1Protocol = "acme-tls/1"
|
import C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
const ACMETLS1Protocol = C.ACMETLS1Protocol
|
||||||
|
|||||||
@@ -18,14 +18,77 @@ import (
|
|||||||
"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
|
||||||
|
Start() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type acmeServiceCertificateProvider struct {
|
||||||
|
ctx context.Context
|
||||||
|
serviceTag string
|
||||||
|
once sync.Once
|
||||||
|
provider adapter.ACMECertificateProvider
|
||||||
|
resolveErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) Start() error {
|
||||||
|
_, err := p.resolveProvider()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
provider, err := p.resolveProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return provider.GetCertificate(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) GetACMENextProtos() []string {
|
||||||
|
provider, err := p.resolveProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return provider.GetACMENextProtos()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) resolveProvider() (adapter.ACMECertificateProvider, error) {
|
||||||
|
p.once.Do(func() {
|
||||||
|
serviceManager := service.FromContext[adapter.ServiceManager](p.ctx)
|
||||||
|
if serviceManager == nil {
|
||||||
|
p.resolveErr = E.New("missing service manager in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerService, found := serviceManager.Get(p.serviceTag)
|
||||||
|
if !found {
|
||||||
|
p.resolveErr = E.New("certificate provider service not found: ", p.serviceTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
provider, ok := providerService.(adapter.ACMECertificateProvider)
|
||||||
|
if !ok {
|
||||||
|
p.resolveErr = E.New("service ", p.serviceTag, " is not an ACME certificate service")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.provider = provider
|
||||||
|
})
|
||||||
|
return p.provider, p.resolveErr
|
||||||
|
}
|
||||||
|
|
||||||
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 +116,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] == 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] == 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 +134,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 +165,24 @@ 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
|
||||||
}
|
}
|
||||||
return nil
|
c.updateProviderNextProtos()
|
||||||
}
|
}
|
||||||
|
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 +286,58 @@ 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()
|
}
|
||||||
|
|
||||||
|
func (c *STDServerConfig) updateProviderNextProtos() {
|
||||||
|
if c.certificateProvider == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if c.watcher != nil {
|
acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider)
|
||||||
return c.watcher.Close()
|
if !isACME {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil
|
nextProtos := acmeProvider.GetACMENextProtos()
|
||||||
|
if len(nextProtos) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
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, 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 {
|
||||||
|
logger.Warn("inline acme configuration is deprecated, use certificate_provider with an ACME service instead")
|
||||||
//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 +390,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 +478,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,
|
||||||
@@ -387,3 +506,19 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newCertificateProvider(ctx context.Context, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
|
||||||
|
switch options.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
serviceTag := options.ACMEOptions.Service
|
||||||
|
if serviceTag == "" {
|
||||||
|
return nil, E.New("missing ACME service tag in certificate_provider")
|
||||||
|
}
|
||||||
|
return &acmeServiceCertificateProvider{
|
||||||
|
ctx: ctx,
|
||||||
|
serviceTag: serviceTag,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate provider type: ", options.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const (
|
|||||||
TypeCCM = "ccm"
|
TypeCCM = "ccm"
|
||||||
TypeOCM = "ocm"
|
TypeOCM = "ocm"
|
||||||
TypeOOMKiller = "oom-killer"
|
TypeOOMKiller = "oom-killer"
|
||||||
|
TypeACME = "acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
3
constant/tls.go
Normal file
3
constant/tls.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const ACMETLS1Protocol = "acme-tls/1"
|
||||||
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/service"
|
||||||
|
"github.com/sagernet/sing-box/service/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerACMEService(registry *service.Registry) {
|
||||||
|
acme.RegisterService(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/service"
|
||||||
|
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 registerACMEService(registry *service.Registry) {
|
||||||
|
service.Register[option.ACMEServiceOptions](registry, C.TypeACME, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMEServiceOptions) (adapter.Service, error) {
|
||||||
|
return nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -130,6 +130,7 @@ func ServiceRegistry() *service.Registry {
|
|||||||
|
|
||||||
resolved.RegisterService(registry)
|
resolved.RegisterService(registry)
|
||||||
ssmapi.RegisterService(registry)
|
ssmapi.RegisterService(registry)
|
||||||
|
registerACMEService(registry)
|
||||||
|
|
||||||
registerDERPService(registry)
|
registerDERPService(registry)
|
||||||
registerCCMService(registry)
|
registerCCMService(registry)
|
||||||
|
|||||||
17
option/acme.go
Normal file
17
option/acme.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
|
type ACMEServiceOptions 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"`
|
||||||
|
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 *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
|
||||||
|
}
|
||||||
53
option/certificate_provider.go
Normal file
53
option/certificate_provider.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _CertificateProviderOptions struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ACMEOptions CertificateProviderACMEOptions `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertificateProviderOptions _CertificateProviderOptions
|
||||||
|
|
||||||
|
func (o CertificateProviderOptions) MarshalJSON() ([]byte, error) {
|
||||||
|
var v any
|
||||||
|
switch o.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
v = o.ACMEOptions
|
||||||
|
case "":
|
||||||
|
return nil, E.New("missing certificate provider type")
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate provider type: ", o.Type)
|
||||||
|
}
|
||||||
|
return badjson.MarshallObjects((_CertificateProviderOptions)(o), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CertificateProviderOptions) UnmarshalJSON(bytes []byte) error {
|
||||||
|
err := json.Unmarshal(bytes, (*_CertificateProviderOptions)(o))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
switch o.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
v = &o.ACMEOptions
|
||||||
|
case "":
|
||||||
|
return E.New("missing certificate provider type")
|
||||||
|
default:
|
||||||
|
return E.New("unknown certificate provider type: ", o.Type)
|
||||||
|
}
|
||||||
|
err = badjson.UnmarshallExcluded(bytes, (*_CertificateProviderOptions)(o), v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertificateProviderACMEOptions struct {
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
@@ -28,9 +28,12 @@ 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
|
||||||
|
|||||||
43
service/acme/logger.go
Normal file
43
service/acme/logger.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//go:build with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logWriter struct {
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) 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 *logWriter) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoderConfig() zapcore.EncoderConfig {
|
||||||
|
config := zap.NewProductionEncoderConfig()
|
||||||
|
config.TimeKey = zapcore.OmitKey
|
||||||
|
return config
|
||||||
|
}
|
||||||
158
service/acme/service.go
Normal file
158
service/acme/service.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//go:build with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/acmedns"
|
||||||
|
"github.com/libdns/alidns"
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterService(registry *boxService.Registry) {
|
||||||
|
boxService.Register[option.ACMEServiceOptions](registry, C.TypeACME, NewService)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.ACMECertificateProvider = (*Service)(nil)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
boxService.Adapter
|
||||||
|
ctx context.Context
|
||||||
|
logger log.ContextLogger
|
||||||
|
config *certmagic.Config
|
||||||
|
cache *certmagic.Cache
|
||||||
|
domain []string
|
||||||
|
nextProtos []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMEServiceOptions) (adapter.Service, error) {
|
||||||
|
var acmeServer string
|
||||||
|
switch options.Provider {
|
||||||
|
case "", "letsencrypt":
|
||||||
|
acmeServer = certmagic.LetsEncryptProductionCA
|
||||||
|
case "zerossl":
|
||||||
|
acmeServer = certmagic.ZeroSSLProductionCA
|
||||||
|
default:
|
||||||
|
if !strings.HasPrefix(options.Provider, "https://") {
|
||||||
|
return nil, E.New("unsupported ACME provider: ", options.Provider)
|
||||||
|
}
|
||||||
|
acmeServer = options.Provider
|
||||||
|
}
|
||||||
|
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(encoderConfig()),
|
||||||
|
&logWriter{logger: logger},
|
||||||
|
zap.DebugLevel,
|
||||||
|
))
|
||||||
|
config := &certmagic.Config{
|
||||||
|
DefaultServerName: options.DefaultServerName,
|
||||||
|
Storage: storage,
|
||||||
|
Logger: zapLogger,
|
||||||
|
}
|
||||||
|
acmeIssuer := certmagic.ACMEIssuer{
|
||||||
|
CA: acmeServer,
|
||||||
|
Email: options.Email,
|
||||||
|
Agreed: true,
|
||||||
|
DisableHTTPChallenge: options.DisableHTTPChallenge,
|
||||||
|
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
|
||||||
|
AltHTTPPort: int(options.AlternativeHTTPPort),
|
||||||
|
AltTLSALPNPort: int(options.AlternativeTLSPort),
|
||||||
|
Logger: zapLogger,
|
||||||
|
}
|
||||||
|
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
|
||||||
|
var solver certmagic.DNS01Solver
|
||||||
|
switch dnsOptions.Provider {
|
||||||
|
case C.DNSProviderAliDNS:
|
||||||
|
solver.DNSProvider = &alidns.Provider{
|
||||||
|
CredentialInfo: alidns.CredentialInfo{
|
||||||
|
AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
|
||||||
|
AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
|
||||||
|
RegionID: dnsOptions.AliDNSOptions.RegionID,
|
||||||
|
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case C.DNSProviderCloudflare:
|
||||||
|
solver.DNSProvider = &cloudflare.Provider{
|
||||||
|
APIToken: dnsOptions.CloudflareOptions.APIToken,
|
||||||
|
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
|
||||||
|
}
|
||||||
|
case C.DNSProviderACMEDNS:
|
||||||
|
solver.DNSProvider = &acmedns.Provider{
|
||||||
|
Username: dnsOptions.ACMEDNSOptions.Username,
|
||||||
|
Password: dnsOptions.ACMEDNSOptions.Password,
|
||||||
|
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
|
||||||
|
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, E.New("unsupported ACME DNS01 provider type: ", dnsOptions.Provider)
|
||||||
|
}
|
||||||
|
acmeIssuer.DNS01Solver = &solver
|
||||||
|
}
|
||||||
|
if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
|
||||||
|
acmeIssuer.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
|
||||||
|
}
|
||||||
|
config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeIssuer)}
|
||||||
|
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: boxService.NewAdapter(C.TypeACME, tag),
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
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
|
||||||
|
}
|
||||||
3
service/acme/stub.go
Normal file
3
service/acme/stub.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//go:build !with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
Reference in New Issue
Block a user