Compare commits

..

16 Commits

Author SHA1 Message Date
世界
025b947a24 Bump version 2026-04-10 16:23:45 +08:00
世界
76fa3c2e5e tun: Fixes 2026-04-10 14:13:06 +08:00
世界
53db1f178c Fix tailscale crash 2026-04-10 14:09:03 +08:00
世界
55ec8abf17 Fix local DNS server for Android 2026-04-10 14:08:57 +08:00
Berkay Özdemirci
5a957fd750 Fix EDNS OPT record corruption in DNS cache
The TTL computation and assignment loops treat OPT record's Hdr.Ttl
as a regular TTL, but per RFC 6891 it encodes EDNS0 metadata
(ExtRCode|Version|Flags). This corrupts cached responses causing
systemd-resolved to reject them with EDNS version 255.

Also fix pointer aliasing: storeCache() stored raw *dns.Msg pointer
so subsequent mutations by Exchange() corrupted cached data.

- Skip OPT records in all TTL loops (Exchange + loadResponse)
- Use message.Copy() in storeCache() to isolate cache from mutations
2026-04-10 14:08:24 +08:00
TargetLocked
7c3d8cf8db Fix disable tcp keep alive 2026-04-10 13:29:15 +08:00
世界
813b634d08 Bump version 2026-04-06 23:09:11 +08:00
hdrover
d9b435fb62 Fix naive inbound padding bytes 2026-04-06 22:33:11 +08:00
世界
354b4b040e sing: Fix vectorised readv iovec length calculation
This does not seem to affect any actual paths in the sing-box.
2026-04-01 16:16:58 +08:00
世界
7ffdc48b49 Bump version 2026-03-30 23:03:43 +08:00
世界
e15bdf11eb sing: Minor fixes 2026-03-30 22:58:11 +08:00
世界
e3bcb06c3e platform: Add HTTPResponse.WriteToWithProgress 2026-03-30 22:42:36 +08:00
世界
84d2280960 quic: Fix protocol client close & Sync hysteria bbr fix 2026-03-30 22:42:36 +08:00
世界
4fd2532b0a Fix naive quic error message 2026-03-30 22:42:36 +08:00
Zhengchao Ding
02ccde6c71 fix(rpm): add vendor field to fpm config to avoid (none) vendor
Co-authored-by: Hyper <hypar@disroot.org>
2026-03-30 22:09:54 +08:00
世界
e98b4ad449 Fix WireGuard shutdown race crashing
Stop peer goroutines before closing the TUN device to prevent
RoutineSequentialReceiver from calling Write on a nil dispatcher.
2026-03-26 16:33:21 +08:00
103 changed files with 308 additions and 5314 deletions

View File

@@ -4,6 +4,7 @@
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes

View File

@@ -1 +1 @@
ea7cd33752aed62603775af3df946c1b83f4b0b3
2fef65f9dba90ddb89a87d00a6eb6165487c10c1

View File

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

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
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
}

View File

@@ -2,7 +2,6 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
@@ -83,8 +82,6 @@ type InboundContext struct {
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool

View File

@@ -1,23 +0,0 @@
package adapter
import (
"net"
"net/netip"
)
type NeighborEntry struct {
Address netip.Addr
MACAddress net.HardwareAddr
Hostname string
}
type NeighborResolver interface {
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
LookupHostname(address netip.Addr) (string, bool)
Start() error
Close() error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries []NeighborEntry)
}

View File

@@ -36,10 +36,6 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {

View File

@@ -26,8 +26,6 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}

133
box.go
View File

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

View File

@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else {
dialer.Timeout = C.TCPConnectTimeout
}
if !options.DisableTCPKeepAlive {
if options.DisableTCPKeepAlive {
dialer.KeepAlive = -1
dialer.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(options.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial

View File

@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
if l.listenOptions.ReuseAddr {
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
}
if !l.listenOptions.DisableTCPKeepAlive {
if l.listenOptions.DisableTCPKeepAlive {
listenConfig.KeepAlive = -1
listenConfig.KeepAliveConfig.Enable = false
} else {
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
if keepIdle == 0 {
keepIdle = C.TCPKeepAliveInitial

View File

@@ -38,6 +38,37 @@ func (w *acmeWrapper) Close() error {
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) {
var acmeServer string
switch options.Provider {
@@ -60,8 +91,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
&ACMELogWriter{Logger: logger},
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
@@ -127,7 +158,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
} else {
tlsConfig = &tls.Config{
GetCertificate: config.GetCertificate,
NextProtos: []string{C.ACMETLS1Protocol},
NextProtos: []string{ACMETLS1Protocol},
}
}
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil

View File

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

View File

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

View File

@@ -32,10 +32,6 @@ type RealityServerConfig struct {
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
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 {
return nil, E.New("acme is unavailable in reality")
}

View File

@@ -13,87 +13,19 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
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/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
)
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 {
access sync.RWMutex
config *tls.Config
logger log.Logger
certificateProvider managedCertificateProvider
acmeService adapter.SimpleLifecycle
certificate []byte
key []byte
@@ -121,17 +53,18 @@ func (c *STDServerConfig) SetServerName(serverName string) {
func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:]
} else {
return c.config.NextProtos
}
return c.config.NextProtos
}
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else {
config.NextProtos = nextProto
@@ -139,18 +72,6 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
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) {
return c.config, nil
}
@@ -170,39 +91,15 @@ func (c *STDServerConfig) Clone() Config {
}
func (c *STDServerConfig) Start() error {
if c.certificateProvider != nil {
err := c.certificateProvider.Start()
if err != nil {
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()
}
}
}
if c.acmeService != nil {
err := c.acmeService.Start()
return c.acmeService.Start()
} else {
err := c.startWatcher()
if err != nil {
return err
c.logger.Warn("create fsnotify watcher: ", err)
}
return nil
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
return nil
}
func (c *STDServerConfig) startWatcher() error {
@@ -306,34 +203,23 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
}
func (c *STDServerConfig) Close() error {
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
if c.acmeService != nil {
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) {
if !options.Enabled {
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 certificateProvider managedCertificateProvider
var acmeService adapter.SimpleLifecycle
var err error
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)
if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil {
@@ -386,7 +272,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
certificate []byte
key []byte
)
if certificateProvider == nil && acmeService == nil {
if acmeService == nil {
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
@@ -474,7 +360,6 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
serverConfig := &STDServerConfig{
config: tlsConfig,
logger: logger,
certificateProvider: certificateProvider,
acmeService: acmeService,
certificate: certificate,
key: key,
@@ -484,8 +369,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
echKeyPath: echKeyPath,
}
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.RLock()
defer serverConfig.access.RUnlock()
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
@@ -502,27 +387,3 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
}
return config, nil
}
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
if options.IsShared() {
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
if manager == nil {
return nil, E.New("missing certificate provider manager in context")
}
return &sharedCertificateProvider{
tag: options.Tag,
manager: manager,
}, nil
}
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if registry == nil {
return nil, E.New("missing certificate provider registry in context")
}
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
if err != nil {
return nil, E.Cause(err, "create inline certificate provider")
}
return &inlineCertificateProvider{
provider: provider,
}, nil
}

View File

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

View File

@@ -283,6 +283,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
@@ -294,6 +297,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
@@ -381,21 +387,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message)
c.cache.Add(question, message.Copy())
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message)
}, message.Copy())
}
} else {
if !c.independentCache {
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message, time.Second*time.Duration(timeToLive))
}, message.Copy(), time.Second*time.Duration(timeToLive))
}
}
}
@@ -486,6 +492,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
@@ -500,12 +509,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL)
}
}

View File

@@ -2,7 +2,15 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.7
#### 1.13.7
* Fixes and improvements
#### 1.13.6
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
@@ -10,25 +18,6 @@ icon: material/alert-decagram
* Fixes and improvements
#### 1.14.0-alpha.4
* Refactor ACME support to certificate provider system **1**
* Add Cloudflare Origin CA certificate provider **2**
* Add Tailscale certificate provider **3**
* Fixes and improvements
**1**:
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
**2**:
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
**3**:
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
@@ -53,59 +42,6 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -154,12 +149,6 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -419,26 +408,6 @@ Matches network interface (same values as `network_type`) address.
Match default interface address.
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### wifi_ssid
!!! quote ""

View File

@@ -2,11 +2,6 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -154,12 +149,6 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -418,26 +407,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配默认接口地址。
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### wifi_ssid
!!! quote ""

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! 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)
!!! quote "Changes in sing-box 1.13.3"
@@ -134,12 +134,6 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -566,30 +560,6 @@ Limit android packages in route.
Exclude android packages in route.
#### include_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Limit MAC addresses in route. Not limited by default.
Conflict with `exclude_mac_address`.
#### exclude_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Exclude MAC addresses in route.
Conflict with `include_mac_address`.
#### platform
Platform-specific settings, provided by client applications.

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "sing-box 1.13.3 中的更改"
:material-alert: [strict_route](#strict_route)
@@ -135,12 +130,6 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -554,30 +543,6 @@ TCP/IP 栈。
排除路由的 Android 应用包名。
#### include_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
限制被路由的 MAC 地址。默认不限制。
`exclude_mac_address` 冲突。
#### exclude_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
排除路由的 MAC 地址。
`include_mac_address` 冲突。
#### platform
平台特定的设置,由客户端应用提供。

View File

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

View File

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

View File

@@ -4,11 +4,6 @@ icon: material/alert-decagram
# Route
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -40,9 +35,6 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_domain_resolver": "", // or {}
"default_network_strategy": "",
"default_network_type": [],
@@ -115,38 +107,6 @@ Set routing mark by default.
Takes no effect if `outbound.routing_mark` is set.
#### find_process
!!! quote ""
Only supported on Linux, Windows, and macOS.
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
#### find_neighbor
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
#### dhcp_lease_files
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Custom DHCP lease file paths for hostname and MAC address resolution.
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
#### default_domain_resolver
!!! question "Since sing-box 1.12.0"

View File

@@ -4,11 +4,6 @@ icon: material/alert-decagram
# 路由
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -42,9 +37,6 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_network_strategy": "",
"default_fallback_delay": ""
}
@@ -114,38 +106,6 @@ icon: material/alert-decagram
如果设置了 `outbound.routing_mark` 设置,则不生效。
#### find_process
!!! quote ""
仅支持 Linux、Windows 和 macOS。
在没有 `process_name``process_path``package_name``user``user_id` 规则时启用进程搜索以输出日志。
#### find_neighbor
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
在没有 `source_mac_address``source_hostname` 规则时启用邻居解析以输出日志。
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
#### dhcp_lease_files
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
为空时自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
#### default_domain_resolver
!!! question "自 sing-box 1.12.0 起"

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -164,12 +159,6 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -460,26 +449,6 @@ Match specified outbounds' preferred routes.
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### rule_set
!!! question "Since sing-box 1.8.0"

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -162,12 +157,6 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -458,26 +447,6 @@ icon: material/new-box
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### rule_set
!!! question "自 sing-box 1.8.0 起"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,14 +2,6 @@
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"
:material-plus: [alidns.security_token](#security_token)
@@ -20,57 +12,12 @@ icon: material/new-box
```json
{
"ttl": "",
"propagation_delay": "",
"propagation_timeout": "",
"resolvers": [],
"override_domain": "",
"provider": "",
... // 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
#### Alibaba Cloud DNS

View File

@@ -2,14 +2,6 @@
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 中的更改"
:material-plus: [alidns.security_token](#security_token)
@@ -20,57 +12,12 @@ icon: material/new-box
```json
{
"ttl": "",
"propagation_delay": "",
"propagation_timeout": "",
"resolvers": [],
"override_domain": "",
"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

View File

@@ -1,49 +0,0 @@
---
icon: material/lan
---
# Neighbor Resolution
Match LAN devices by MAC address and hostname using
[`source_mac_address`](/configuration/route/rule/#source_mac_address) and
[`source_hostname`](/configuration/route/rule/#source_hostname) rule items.
Neighbor resolution is automatically enabled when these rule items exist.
Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules.
## Linux
Works natively. No special setup required.
Hostname resolution requires DHCP lease files,
automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea).
Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files).
## Android
!!! quote ""
Only supported in graphical clients.
Requires Android 11 or above and ROOT.
Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection.
ROM built-in features like "Use VPN for connected devices" can share VPN
but cannot provide MAC address or hostname information.
Set **IP Masquerade Mode** to **None** in VPNHotspot settings.
Only route/DNS rules are supported. TUN include/exclude routes are not supported.
### Hostname Visibility
Hostname is only visible in sing-box if it is visible in VPNHotspot.
For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings
of the connected network. Non-Apple devices are always visible.
## macOS
Requires the standalone version (macOS system extension).
The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading.
See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup.

View File

@@ -1,49 +0,0 @@
---
icon: material/lan
---
# 邻居解析
通过
[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和
[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。
当这些规则项存在时,邻居解析自动启用。
使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。
## Linux
原生支持,无需特殊设置。
主机名解析需要 DHCP 租约文件,
自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。
## Android
!!! quote ""
仅在图形客户端中支持。
需要 Android 11 或以上版本和 ROOT。
必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。
ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN
但无法提供 MAC 地址或主机名信息。
在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**
仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。
### 设备可见性
MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。
对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。
非 Apple 设备始终可见。
## macOS
需要独立版本macOS 系统扩展)。
App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。
参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。

View File

@@ -2,11 +2,6 @@
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"
:material-plus: [kernel_tx](#kernel_tx)
@@ -54,10 +49,6 @@ icon: material/new-box
"key_path": "",
"kernel_tx": false,
"kernel_rx": false,
"certificate_provider": "",
// Deprecated
"acme": {
"domain": [],
"data_directory": "",
@@ -417,18 +408,6 @@ Enable kernel TLS transmit 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
!!! info "QUIC support"
@@ -490,7 +469,7 @@ The ECH key and configuration can be generated by `sing-box generate ech-keypair
!!! failure "Deprecated in sing-box 1.12.0"
`pq_signature_schemes_enabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.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.
Enable support for post-quantum peer certificate signature schemes.
@@ -498,7 +477,7 @@ Enable support for post-quantum peer certificate signature schemes.
!!! failure "Deprecated in sing-box 1.12.0"
`dynamic_record_sizing_disabled` is deprecated in sing-box 1.12.0 and removed in sing-box 1.13.0.
`dynamic_record_sizing_disabled` has nothing to do with ECH, was added by mistake, has been deprecated and no longer works.
Disables adaptive sizing of TLS records.
@@ -587,10 +566,6 @@ Fragment TLS handshake into multiple TLS records to bypass firewalls.
### 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
List of domain.
@@ -702,4 +677,4 @@ A hexadecimal string with zero to eight digits.
The maximum time difference between the server and the client.
Check disabled if empty.
Check disabled if empty.

View File

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

View File

@@ -4,16 +4,6 @@ icon: material/delete-alert
# 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
#### Legacy DNS server formats
@@ -38,7 +28,7 @@ so `pq_signature_schemes_enabled` has been deprecated and no longer works.
Also, `dynamic_record_sizing_disabled` has nothing to do with ECH,
was added by mistake, has been deprecated and no longer works.
These fields were removed in sing-box 1.13.0.
These fields will be removed in sing-box 1.13.0.
## 1.11.0
@@ -48,7 +38,7 @@ Legacy special outbounds (`block` / `dns`) are deprecated
and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions).
Old fields were removed in sing-box 1.13.0.
Old fields will be removed in sing-box 1.13.0.
#### Legacy inbound fields
@@ -56,7 +46,7 @@ Legacy inbound fields `inbound.<sniff/domain_strategy/...>` are deprecated
and can be replaced by rule actions,
check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions).
Old fields were removed in sing-box 1.13.0.
Old fields will be removed in sing-box 1.13.0.
#### Destination override fields in direct outbound
@@ -64,20 +54,18 @@ Destination override fields (`override_address` / `override_port`) in direct out
and can be replaced by rule actions,
check [Migration](../migration/#migrate-destination-override-fields-to-route-options).
Old fields were removed in sing-box 1.13.0.
#### WireGuard outbound
WireGuard outbound is deprecated and can be replaced by endpoint,
check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint).
Old outbound was removed in sing-box 1.13.0.
Old outbound will be removed in sing-box 1.13.0.
#### GSO option in TUN
GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN.
Old fields were removed in sing-box 1.13.0.
Old fields will be removed in sing-box 1.13.0.
## 1.10.0
@@ -87,12 +75,12 @@ Old fields were removed in sing-box 1.13.0.
`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`.
Old fields were removed in sing-box 1.12.0.
Old fields will be removed in sing-box 1.12.0.
#### Match source rule items are renamed
`rule_set_ipcidr_match_source` route and DNS rule items are renamed to
`rule_set_ip_cidr_match_source` and were removed in sing-box 1.11.0.
`rule_set_ip_cidr_match_source` and will be remove in sing-box 1.11.0.
#### Drop support for go1.18 and go1.19
@@ -107,7 +95,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o
#### GeoIP
GeoIP is deprecated and was removed in sing-box 1.12.0.
GeoIP is deprecated and will be removed in sing-box 1.12.0.
The maxmind GeoIP National Database, as an IP classification database,
is not entirely suitable for traffic bypassing,
@@ -118,7 +106,7 @@ check [Migration](/migration/#migrate-geoip-to-rule-sets).
#### Geosite
Geosite is deprecated and was removed in sing-box 1.12.0.
Geosite is deprecated and will be removed in sing-box 1.12.0.
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.

View File

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

View File

@@ -2,83 +2,6 @@
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
### Migrate to new DNS server formats

View File

@@ -2,83 +2,6 @@
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
### 迁移到新的 DNS 服务器格式

View File

@@ -102,20 +102,10 @@ var OptionLegacyDomainStrategyOptions = Note{
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{
OptionLegacyDNSTransport,
OptionLegacyDNSFakeIPOptions,
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
OptionLegacyDomainStrategyOptions,
OptionInlineACME,
}

View File

@@ -33,7 +33,7 @@ func baseContext(platformInterface PlatformInterface) context.Context {
}
ctx := context.Background()
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry())
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
}
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
@@ -144,18 +144,6 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil
}
func (s *platformInterfaceStub) UsePlatformNeighborResolver() bool {
return false
}
func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return os.ErrInvalid
}
func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return nil
}
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
return false
}

View File

@@ -52,6 +52,11 @@ type HTTPRequest interface {
type HTTPResponse interface {
GetContent() (*StringBox, error)
WriteTo(path string) error
WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error
}
type HTTPResponseWriteToProgressHandler interface {
Update(progress int64, total int64)
}
var (
@@ -239,3 +244,31 @@ func (h *httpResponse) WriteTo(path string) error {
defer file.Close()
return common.Error(bufio.Copy(file, h.Body))
}
func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error {
defer h.Body.Close()
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return common.Error(bufio.Copy(&progressWriter{
writer: file,
handler: handler,
total: h.ContentLength,
}, h.Body))
}
type progressWriter struct {
writer io.Writer
handler HTTPResponseWriteToProgressHandler
total int64
written int64
}
func (w *progressWriter) Write(p []byte) (int, error) {
n, err := w.writer.Write(p)
w.written += int64(n)
w.handler.Update(w.written, w.total)
return n, err
}

View File

@@ -1,53 +0,0 @@
package libbox
import (
"net"
"net/netip"
)
type NeighborEntry struct {
Address string
MacAddress string
Hostname string
}
type NeighborEntryIterator interface {
Next() *NeighborEntry
HasNext() bool
}
type NeighborSubscription struct {
done chan struct{}
}
func (s *NeighborSubscription) Close() {
close(s.done)
}
func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator {
entries := make([]*NeighborEntry, 0, len(table))
for address, mac := range table {
entries = append(entries, &NeighborEntry{
Address: address.String(),
MacAddress: mac.String(),
})
}
return &neighborEntryIterator{entries}
}
type neighborEntryIterator struct {
entries []*NeighborEntry
}
func (i *neighborEntryIterator) HasNext() bool {
return len(i.entries) > 0
}
func (i *neighborEntryIterator) Next() *NeighborEntry {
if len(i.entries) == 0 {
return nil
}
entry := i.entries[0]
i.entries = i.entries[1:]
return entry
}

View File

@@ -1,123 +0,0 @@
//go:build darwin
package libbox
import (
"net"
"net/netip"
"os"
"slices"
"time"
"github.com/sagernet/sing-box/route"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
xroute "golang.org/x/net/route"
"golang.org/x/sys/unix"
)
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
entries, err := route.ReadNeighborEntries()
if err != nil {
return nil, E.Cause(err, "initial neighbor dump")
}
table := make(map[netip.Addr]net.HardwareAddr)
for _, entry := range entries {
table[entry.Address] = entry.MACAddress
}
listener.UpdateNeighborTable(tableToIterator(table))
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return nil, E.Cause(err, "open route socket")
}
err = unix.SetNonblock(routeSocket, true)
if err != nil {
unix.Close(routeSocket)
return nil, E.Cause(err, "set route socket nonblock")
}
subscription := &NeighborSubscription{
done: make(chan struct{}),
}
go subscription.loop(listener, routeSocket, table)
return subscription, nil
}
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, routeSocket int, table map[netip.Addr]net.HardwareAddr) {
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
defer routeSocketFile.Close()
buffer := buf.NewPacket()
defer buffer.Release()
for {
select {
case <-s.done:
return
default:
}
tv := unix.NsecToTimeval(int64(3 * time.Second))
_ = unix.SetsockoptTimeval(routeSocket, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
n, err := routeSocketFile.Read(buffer.FreeBytes())
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
select {
case <-s.done:
return
default:
}
continue
}
messages, err := xroute.ParseRIB(xroute.RIBTypeRoute, buffer.FreeBytes()[:n])
if err != nil {
continue
}
changed := false
for _, message := range messages {
routeMessage, isRouteMessage := message.(*xroute.RouteMessage)
if !isRouteMessage {
continue
}
if routeMessage.Flags&unix.RTF_LLINFO == 0 {
continue
}
address, mac, isDelete, ok := route.ParseRouteNeighborMessage(routeMessage)
if !ok {
continue
}
if isDelete {
if _, exists := table[address]; exists {
delete(table, address)
changed = true
}
} else {
existing, exists := table[address]
if !exists || !slices.Equal(existing, mac) {
table[address] = mac
changed = true
}
}
}
if changed {
listener.UpdateNeighborTable(tableToIterator(table))
}
}
}
func ReadBootpdLeases() NeighborEntryIterator {
leaseIPToMAC, ipToHostname, macToHostname := route.ReloadLeaseFiles([]string{"/var/db/dhcpd_leases"})
entries := make([]*NeighborEntry, 0, len(leaseIPToMAC))
for address, mac := range leaseIPToMAC {
entry := &NeighborEntry{
Address: address.String(),
MacAddress: mac.String(),
}
hostname, found := ipToHostname[address]
if !found {
hostname = macToHostname[mac.String()]
}
entry.Hostname = hostname
entries = append(entries, entry)
}
return &neighborEntryIterator{entries}
}

View File

@@ -1,88 +0,0 @@
//go:build linux
package libbox
import (
"net"
"net/netip"
"slices"
"time"
"github.com/sagernet/sing-box/route"
E "github.com/sagernet/sing/common/exceptions"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
entries, err := route.ReadNeighborEntries()
if err != nil {
return nil, E.Cause(err, "initial neighbor dump")
}
table := make(map[netip.Addr]net.HardwareAddr)
for _, entry := range entries {
table[entry.Address] = entry.MACAddress
}
listener.UpdateNeighborTable(tableToIterator(table))
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
})
if err != nil {
return nil, E.Cause(err, "subscribe neighbor updates")
}
subscription := &NeighborSubscription{
done: make(chan struct{}),
}
go subscription.loop(listener, connection, table)
return subscription, nil
}
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) {
defer connection.Close()
for {
select {
case <-s.done:
return
default:
}
err := connection.SetReadDeadline(time.Now().Add(3 * time.Second))
if err != nil {
return
}
messages, err := connection.Receive()
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
select {
case <-s.done:
return
default:
}
continue
}
changed := false
for _, message := range messages {
address, mac, isDelete, ok := route.ParseNeighborMessage(message)
if !ok {
continue
}
if isDelete {
if _, exists := table[address]; exists {
delete(table, address)
changed = true
}
} else {
existing, exists := table[address]
if !exists || !slices.Equal(existing, mac) {
table[address] = mac
changed = true
}
}
}
if changed {
listener.UpdateNeighborTable(tableToIterator(table))
}
}
}

View File

@@ -1,9 +0,0 @@
//go:build !linux && !darwin
package libbox
import "os"
func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) {
return nil, os.ErrInvalid
}

View File

@@ -21,13 +21,6 @@ type PlatformInterface interface {
SystemCertificates() StringIterator
ClearDNSCache()
SendNotification(notification *Notification) error
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
RegisterMyInterface(name string)
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries NeighborEntryIterator)
}
type ConnectionOwner struct {

View File

@@ -78,7 +78,6 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO
}
options.FileDescriptor = dupFd
w.myTunName = options.Name
w.iif.RegisterMyInterface(options.Name)
return tun.New(*options)
}
@@ -221,46 +220,6 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi
return w.iif.SendNotification((*Notification)(notification))
}
func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool {
return true
}
func (w *platformInterfaceWrapper) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return w.iif.StartNeighborMonitor(&neighborUpdateListenerWrapper{listener: listener})
}
func (w *platformInterfaceWrapper) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return w.iif.CloseNeighborMonitor(nil)
}
type neighborUpdateListenerWrapper struct {
listener adapter.NeighborUpdateListener
}
func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntryIterator) {
var result []adapter.NeighborEntry
for entries.HasNext() {
entry := entries.Next()
if entry == nil {
continue
}
address, err := netip.ParseAddr(entry.Address)
if err != nil {
continue
}
macAddress, err := net.ParseMAC(entry.MacAddress)
if err != nil {
continue
}
result = append(result, adapter.NeighborEntry{
Address: address,
MACAddress: macAddress,
Hostname: entry.Hostname,
})
}
w.listener.UpdateNeighborTable(result)
}
func AvailablePort(startPort int32) (int32, error) {
for port := int(startPort); ; port++ {
if port > 65535 {

14
go.mod
View File

@@ -14,13 +14,11 @@ require (
github.com/godbus/dbus/v5 v5.2.2
github.com/gofrs/uuid/v5 v5.4.0
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91
github.com/jsimonetti/rtnetlink v1.4.0
github.com/keybase/go-keychain v0.0.1
github.com/libdns/acmedns v0.5.0
github.com/libdns/alidns v1.0.6
github.com/libdns/cloudflare v0.2.2
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mdlayher/netlink v1.9.0
github.com/metacubex/utls v1.8.4
github.com/mholt/acmez/v3 v3.1.6
github.com/miekg/dns v1.1.72
@@ -29,19 +27,19 @@ require (
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
github.com/sagernet/sing v0.8.4
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.0
github.com/sagernet/sing-quic v0.6.1
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d
github.com/sagernet/sing-tun v0.8.7
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7
@@ -94,9 +92,11 @@ require (
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/libdns/libdns v1.1.1 // indirect
github.com/mdlayher/netlink v1.9.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect

20
go.sum
View File

@@ -162,10 +162,10 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc h1:YK7PwJT0irRAEui9ASdXSxcE2BOVQipWMF/A1Ogt+7c=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc h1:EJPHOqk23IuBsTjXK9OXqkNxPbKOBWKRmviQoCcriAs=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc/go.mod h1:8aty0RW96DrJSMWXO6bRPMBJEjuqq5JWiOIi4bCRzFA=
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054=
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E=
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
@@ -236,20 +236,20 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI=
github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw=
github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d h1:vi0j6301f6H8t2GYgAC2PA2AdnGdMwkP34B4+N03Qt4=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.7 h1:q49cI7Cbp+BcgzaJitQ9QdLO77BqnnaQRkSEMoGmF3g=
github.com/sagernet/sing-tun v0.8.7/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=

View File

@@ -1,12 +0,0 @@
//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)
}

View File

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

View File

@@ -5,7 +5,6 @@ import (
"github.com/sagernet/sing-box"
"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/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
@@ -35,14 +34,13 @@ import (
"github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless"
"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/ssmapi"
E "github.com/sagernet/sing/common/exceptions"
)
func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry())
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
}
func InboundRegistry() *inbound.Registry {
@@ -141,16 +139,6 @@ func ServiceRegistry() *service.Registry {
return registry
}
func CertificateProviderRegistry() *certificate.Registry {
registry := certificate.NewRegistry()
registerACMECertificateProvider(registry)
registerTailscaleCertificateProvider(registry)
originca.RegisterCertificateProvider(registry)
return 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) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

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

View File

@@ -6,7 +6,6 @@ import (
"context"
"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/service"
C "github.com/sagernet/sing-box/constant"
@@ -28,12 +27,6 @@ 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) {
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`)

View File

@@ -122,11 +122,6 @@ nav:
- Listen Fields: configuration/shared/listen.md
- Dial Fields: configuration/shared/dial.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
- Pre-match: configuration/shared/pre-match.md
- Multiplex: configuration/shared/multiplex.md
@@ -134,7 +129,6 @@ nav:
- UDP over TCP: configuration/shared/udp-over-tcp.md
- TCP Brutal: configuration/shared/tcp-brutal.md
- Wi-Fi State: configuration/shared/wifi-state.md
- Neighbor Resolution: configuration/shared/neighbor.md
- Endpoint:
- configuration/endpoint/index.md
- WireGuard: configuration/endpoint/wireguard.md
@@ -278,7 +272,6 @@ plugins:
Shared: 通用
Listen Fields: 监听字段
Dial Fields: 拨号字段
Certificate Provider Fields: 证书提供者字段
DNS01 Challenge Fields: DNS01 验证字段
Multiplex: 多路复用
V2Ray Transport: V2Ray 传输层
@@ -287,7 +280,6 @@ plugins:
Endpoint: 端点
Inbound: 入站
Outbound: 出站
Certificate Provider: 证书提供者
Manual: 手册
reconfigure_material: true

View File

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

View File

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

View File

@@ -10,19 +10,18 @@ import (
)
type _Options struct {
RawMessage json.RawMessage `json:"-"`
Schema string `json:"$schema,omitempty"`
Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
CertificateProviders []CertificateProvider `json:"certificate_providers,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
RawMessage json.RawMessage `json:"-"`
Schema string `json:"$schema,omitempty"`
Log *LogOptions `json:"log,omitempty"`
DNS *DNSOptions `json:"dns,omitempty"`
NTP *NTPOptions `json:"ntp,omitempty"`
Certificate *CertificateOptions `json:"certificate,omitempty"`
Endpoints []Endpoint `json:"endpoints,omitempty"`
Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
}
type Options _Options
@@ -57,25 +56,6 @@ func checkOptions(options *Options) error {
if err != nil {
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
}

View File

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

View File

@@ -9,8 +9,6 @@ type RouteOptions struct {
RuleSet []RuleSet `json:"rule_set,omitempty"`
Final string `json:"final,omitempty"`
FindProcess bool `json:"find_process,omitempty"`
FindNeighbor bool `json:"find_neighbor,omitempty"`
DHCPLeaseFiles badoption.Listable[string] `json:"dhcp_lease_files,omitempty"`
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
DefaultInterface string `json:"default_interface,omitempty"`

View File

@@ -103,8 +103,6 @@ type RawDefaultRule struct {
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`

View File

@@ -106,8 +106,6 @@ type RawDefaultDNSRule struct {
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`

View File

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

View File

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

View File

@@ -39,8 +39,6 @@ type TunInboundOptions struct {
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"`
ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"`
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
Stack string `json:"stack,omitempty"`
Platform *TunPlatformOptions `json:"platform,omitempty"`

View File

@@ -29,7 +29,10 @@ import (
"golang.org/x/net/http2/h2c"
)
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
var (
ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
WrapError func(error) error
)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound)

View File

@@ -95,7 +95,7 @@ func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, er
binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(data))
buffer.Extend(paddingSize)
common.Must(buffer.WriteZeroN(paddingSize))
_, err = writer.Write(buffer.Bytes())
if err == nil {
n = len(data)
@@ -117,7 +117,7 @@ func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffe
header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(paddingSize)
buffer.Extend(paddingSize)
common.Must(buffer.WriteZeroN(paddingSize))
p.writePadding++
}
return common.Error(writer.Write(buffer.Bytes()))
@@ -179,18 +179,18 @@ type naiveConn struct {
func (c *naiveConn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.Conn, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveConn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.Conn, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
err := c.writeBufferWithPadding(c.Conn, buffer)
return baderror.WrapH2(err)
return wrapError(err)
}
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
@@ -210,7 +210,7 @@ type naiveH2Conn struct {
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.reader, p)
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
@@ -218,7 +218,7 @@ func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
if err == nil {
c.flusher.Flush()
}
return n, baderror.WrapH2(err)
return n, wrapError(err)
}
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
@@ -227,7 +227,15 @@ func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
if err == nil {
c.flusher.Flush()
}
return baderror.WrapH2(err)
return wrapError(err)
}
func wrapError(err error) error {
err = baderror.WrapH2(err)
if WrapError != nil {
err = WrapError(err)
}
return err
}
func (c *naiveH2Conn) Close() error {

View File

@@ -124,4 +124,5 @@ func init() {
return quicListener, nil
}
naive.WrapError = qtls.WrapError
}

View File

@@ -1,98 +0,0 @@
//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)
}

View File

@@ -262,9 +262,16 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
}
func (t *Endpoint) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
switch stage {
case adapter.StartStateStart:
return t.start()
case adapter.StartStatePostStart:
return t.postStart()
}
return nil
}
func (t *Endpoint) start() error {
if t.platformInterface != nil {
err := t.network.UpdateInterfaces()
if err != nil {
@@ -347,6 +354,10 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
})
})
}
return nil
}
func (t *Endpoint) postStart() error {
err := t.server.Start()
if err != nil {
if t.systemTun != nil {
@@ -471,13 +482,13 @@ func (t *Endpoint) watchState() {
}
func (t *Endpoint) Close() error {
err := common.Close(common.PtrOrNil(t.server))
netmon.RegisterInterfaceGetter(nil)
netns.SetControlFunc(nil)
if t.fallbackTCPCloser != nil {
t.fallbackTCPCloser()
t.fallbackTCPCloser = nil
}
err := common.Close(common.PtrOrNil(t.server))
if t.systemTun != nil {
t.systemTun.Close()
t.systemTun = nil

View File

@@ -160,22 +160,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if nfQueue == 0 {
nfQueue = tun.DefaultAutoRedirectNFQueue
}
var includeMACAddress []net.HardwareAddr
for i, macString := range options.IncludeMACAddress {
mac, macErr := net.ParseMAC(macString)
if macErr != nil {
return nil, E.Cause(macErr, "parse include_mac_address[", i, "]")
}
includeMACAddress = append(includeMACAddress, mac)
}
var excludeMACAddress []net.HardwareAddr
for i, macString := range options.ExcludeMACAddress {
mac, macErr := net.ParseMAC(macString)
if macErr != nil {
return nil, E.Cause(macErr, "parse exclude_mac_address[", i, "]")
}
excludeMACAddress = append(excludeMACAddress, mac)
}
networkManager := service.FromContext[adapter.NetworkManager](ctx)
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000))
inbound := &Inbound{
@@ -213,8 +197,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage,
IncludeMACAddress: includeMACAddress,
ExcludeMACAddress: excludeMACAddress,
InterfaceMonitor: networkManager.InterfaceMonitor(),
EXP_MultiPendingPackets: multiPendingPackets,
},

View File

@@ -1,239 +0,0 @@
//go:build darwin
package route
import (
"net"
"net/netip"
"os"
"sync"
"time"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
var defaultLeaseFiles = []string{
"/var/db/dhcpd_leases",
"/tmp/dhcp.leases",
}
type neighborResolver struct {
logger logger.ContextLogger
leaseFiles []string
access sync.RWMutex
neighborIPToMAC map[netip.Addr]net.HardwareAddr
leaseIPToMAC map[netip.Addr]net.HardwareAddr
ipToHostname map[netip.Addr]string
macToHostname map[string]string
watcher *fswatch.Watcher
done chan struct{}
}
func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) {
if len(leaseFiles) == 0 {
for _, path := range defaultLeaseFiles {
info, err := os.Stat(path)
if err == nil && info.Size() > 0 {
leaseFiles = append(leaseFiles, path)
}
}
}
return &neighborResolver{
logger: resolverLogger,
leaseFiles: leaseFiles,
neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr),
leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr),
ipToHostname: make(map[netip.Addr]string),
macToHostname: make(map[string]string),
done: make(chan struct{}),
}, nil
}
func (r *neighborResolver) Start() error {
err := r.loadNeighborTable()
if err != nil {
r.logger.Warn(E.Cause(err, "load neighbor table"))
}
r.doReloadLeaseFiles()
go r.subscribeNeighborUpdates()
if len(r.leaseFiles) > 0 {
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: r.leaseFiles,
Logger: r.logger,
Callback: func(_ string) {
r.doReloadLeaseFiles()
},
})
if err != nil {
r.logger.Warn(E.Cause(err, "create lease file watcher"))
} else {
r.watcher = watcher
err = watcher.Start()
if err != nil {
r.logger.Warn(E.Cause(err, "start lease file watcher"))
}
}
}
return nil
}
func (r *neighborResolver) Close() error {
close(r.done)
if r.watcher != nil {
return r.watcher.Close()
}
return nil
}
func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
r.access.RLock()
defer r.access.RUnlock()
mac, found := r.neighborIPToMAC[address]
if found {
return mac, true
}
mac, found = r.leaseIPToMAC[address]
if found {
return mac, true
}
mac, found = extractMACFromEUI64(address)
if found {
return mac, true
}
return nil, false
}
func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) {
r.access.RLock()
defer r.access.RUnlock()
hostname, found := r.ipToHostname[address]
if found {
return hostname, true
}
mac, macFound := r.neighborIPToMAC[address]
if !macFound {
mac, macFound = r.leaseIPToMAC[address]
}
if !macFound {
mac, macFound = extractMACFromEUI64(address)
}
if macFound {
hostname, found = r.macToHostname[mac.String()]
if found {
return hostname, true
}
}
return "", false
}
func (r *neighborResolver) loadNeighborTable() error {
entries, err := ReadNeighborEntries()
if err != nil {
return err
}
r.access.Lock()
defer r.access.Unlock()
for _, entry := range entries {
r.neighborIPToMAC[entry.Address] = entry.MACAddress
}
return nil
}
func (r *neighborResolver) subscribeNeighborUpdates() {
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
r.logger.Warn(E.Cause(err, "subscribe neighbor updates"))
return
}
err = unix.SetNonblock(routeSocket, true)
if err != nil {
unix.Close(routeSocket)
r.logger.Warn(E.Cause(err, "set route socket nonblock"))
return
}
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
defer routeSocketFile.Close()
buffer := buf.NewPacket()
defer buffer.Release()
for {
select {
case <-r.done:
return
default:
}
err = setReadDeadline(routeSocketFile, 3*time.Second)
if err != nil {
r.logger.Warn(E.Cause(err, "set route socket read deadline"))
return
}
n, err := routeSocketFile.Read(buffer.FreeBytes())
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
select {
case <-r.done:
return
default:
}
r.logger.Warn(E.Cause(err, "receive neighbor update"))
continue
}
messages, err := route.ParseRIB(route.RIBTypeRoute, buffer.FreeBytes()[:n])
if err != nil {
continue
}
for _, message := range messages {
routeMessage, isRouteMessage := message.(*route.RouteMessage)
if !isRouteMessage {
continue
}
if routeMessage.Flags&unix.RTF_LLINFO == 0 {
continue
}
address, mac, isDelete, ok := ParseRouteNeighborMessage(routeMessage)
if !ok {
continue
}
r.access.Lock()
if isDelete {
delete(r.neighborIPToMAC, address)
} else {
r.neighborIPToMAC[address] = mac
}
r.access.Unlock()
}
}
}
func (r *neighborResolver) doReloadLeaseFiles() {
leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles)
r.access.Lock()
r.leaseIPToMAC = leaseIPToMAC
r.ipToHostname = ipToHostname
r.macToHostname = macToHostname
r.access.Unlock()
}
func setReadDeadline(file *os.File, timeout time.Duration) error {
rawConn, err := file.SyscallConn()
if err != nil {
return err
}
var controlErr error
err = rawConn.Control(func(fd uintptr) {
tv := unix.NsecToTimeval(int64(timeout))
controlErr = unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
})
if err != nil {
return err
}
return controlErr
}

View File

@@ -1,386 +0,0 @@
package route
import (
"bufio"
"encoding/hex"
"net"
"net/netip"
"os"
"strconv"
"strings"
"time"
)
func parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
file, err := os.Open(path)
if err != nil {
return
}
defer file.Close()
if strings.HasSuffix(path, "dhcpd_leases") {
parseBootpdLeases(file, ipToMAC, ipToHostname, macToHostname)
return
}
if strings.HasSuffix(path, "kea-leases4.csv") {
parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname)
return
}
if strings.HasSuffix(path, "kea-leases6.csv") {
parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname)
return
}
if strings.HasSuffix(path, "dhcpd.leases") {
parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname)
return
}
parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname)
}
func ReloadLeaseFiles(leaseFiles []string) (leaseIPToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
leaseIPToMAC = make(map[netip.Addr]net.HardwareAddr)
ipToHostname = make(map[netip.Addr]string)
macToHostname = make(map[string]string)
for _, path := range leaseFiles {
parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname)
}
return
}
func parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
now := time.Now().Unix()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "duid ") {
continue
}
if strings.HasPrefix(line, "# ") {
parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname)
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
expiry, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
continue
}
if expiry != 0 && expiry < now {
continue
}
if strings.Contains(fields[1], ":") {
mac, macErr := net.ParseMAC(fields[1])
if macErr != nil {
continue
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
if !addrOK {
continue
}
address = address.Unmap()
ipToMAC[address] = mac
hostname := fields[3]
if hostname != "*" {
ipToHostname[address] = hostname
macToHostname[mac.String()] = hostname
}
} else {
var mac net.HardwareAddr
if len(fields) >= 5 {
duid, duidErr := parseDUID(fields[4])
if duidErr == nil {
mac, _ = extractMACFromDUID(duid)
}
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
if !addrOK {
continue
}
address = address.Unmap()
if mac != nil {
ipToMAC[address] = mac
}
hostname := fields[3]
if hostname != "*" {
ipToHostname[address] = hostname
if mac != nil {
macToHostname[mac.String()] = hostname
}
}
}
}
}
func parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
fields := strings.Fields(line)
if len(fields) < 5 {
return
}
validTime, err := strconv.ParseInt(fields[4], 10, 64)
if err != nil {
return
}
if validTime == 0 {
return
}
if validTime > 0 && validTime < time.Now().Unix() {
return
}
hostname := fields[3]
if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) {
hostname = ""
}
if len(fields) >= 8 && fields[2] == "ipv4" {
mac, macErr := net.ParseMAC(fields[1])
if macErr != nil {
return
}
addressField := fields[7]
slashIndex := strings.IndexByte(addressField, '/')
if slashIndex >= 0 {
addressField = addressField[:slashIndex]
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
if !addrOK {
return
}
address = address.Unmap()
ipToMAC[address] = mac
if hostname != "" {
ipToHostname[address] = hostname
macToHostname[mac.String()] = hostname
}
return
}
var mac net.HardwareAddr
duidHex := fields[1]
duidBytes, hexErr := hex.DecodeString(duidHex)
if hexErr == nil {
mac, _ = extractMACFromDUID(duidBytes)
}
for i := 7; i < len(fields); i++ {
addressField := fields[i]
slashIndex := strings.IndexByte(addressField, '/')
if slashIndex >= 0 {
addressField = addressField[:slashIndex]
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
if !addrOK {
continue
}
address = address.Unmap()
if mac != nil {
ipToMAC[address] = mac
}
if hostname != "" {
ipToHostname[address] = hostname
if mac != nil {
macToHostname[mac.String()] = hostname
}
}
}
}
func parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
scanner := bufio.NewScanner(file)
var currentIP netip.Addr
var currentMAC net.HardwareAddr
var currentHostname string
var currentActive bool
var inLease bool
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") {
ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {")
parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString))
if addrOK {
currentIP = parsed.Unmap()
inLease = true
currentMAC = nil
currentHostname = ""
currentActive = false
}
continue
}
if line == "}" && inLease {
if currentActive && currentMAC != nil {
ipToMAC[currentIP] = currentMAC
if currentHostname != "" {
ipToHostname[currentIP] = currentHostname
macToHostname[currentMAC.String()] = currentHostname
}
} else {
delete(ipToMAC, currentIP)
delete(ipToHostname, currentIP)
}
inLease = false
continue
}
if !inLease {
continue
}
if strings.HasPrefix(line, "hardware ethernet ") {
macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";")
parsed, macErr := net.ParseMAC(macString)
if macErr == nil {
currentMAC = parsed
}
} else if strings.HasPrefix(line, "client-hostname ") {
hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";")
hostname = strings.Trim(hostname, "\"")
if hostname != "" {
currentHostname = hostname
}
} else if strings.HasPrefix(line, "binding state ") {
state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";")
currentActive = state == "active"
}
}
}
func parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
scanner := bufio.NewScanner(file)
firstLine := true
for scanner.Scan() {
if firstLine {
firstLine = false
continue
}
fields := strings.Split(scanner.Text(), ",")
if len(fields) < 10 {
continue
}
if fields[9] != "0" {
continue
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
if !addrOK {
continue
}
address = address.Unmap()
mac, macErr := net.ParseMAC(fields[1])
if macErr != nil {
continue
}
ipToMAC[address] = mac
hostname := ""
if len(fields) > 8 {
hostname = fields[8]
}
if hostname != "" {
ipToHostname[address] = hostname
macToHostname[mac.String()] = hostname
}
}
}
func parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
scanner := bufio.NewScanner(file)
firstLine := true
for scanner.Scan() {
if firstLine {
firstLine = false
continue
}
fields := strings.Split(scanner.Text(), ",")
if len(fields) < 14 {
continue
}
if fields[13] != "0" {
continue
}
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
if !addrOK {
continue
}
address = address.Unmap()
var mac net.HardwareAddr
if fields[12] != "" {
mac, _ = net.ParseMAC(fields[12])
}
if mac == nil {
duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", ""))
if duidErr == nil {
mac, _ = extractMACFromDUID(duid)
}
}
hostname := ""
if len(fields) > 11 {
hostname = fields[11]
}
if mac != nil {
ipToMAC[address] = mac
}
if hostname != "" {
ipToHostname[address] = hostname
if mac != nil {
macToHostname[mac.String()] = hostname
}
}
}
}
func parseBootpdLeases(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
now := time.Now().Unix()
scanner := bufio.NewScanner(file)
var currentName string
var currentIP netip.Addr
var currentMAC net.HardwareAddr
var currentLease int64
var inBlock bool
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "{" {
inBlock = true
currentName = ""
currentIP = netip.Addr{}
currentMAC = nil
currentLease = 0
continue
}
if line == "}" && inBlock {
if currentMAC != nil && currentIP.IsValid() {
if currentLease == 0 || currentLease >= now {
ipToMAC[currentIP] = currentMAC
if currentName != "" {
ipToHostname[currentIP] = currentName
macToHostname[currentMAC.String()] = currentName
}
}
}
inBlock = false
continue
}
if !inBlock {
continue
}
key, value, found := strings.Cut(line, "=")
if !found {
continue
}
switch key {
case "name":
currentName = value
case "ip_address":
parsed, addrOK := netip.AddrFromSlice(net.ParseIP(value))
if addrOK {
currentIP = parsed.Unmap()
}
case "hw_address":
typeAndMAC, hasSep := strings.CutPrefix(value, "1,")
if hasSep {
mac, macErr := net.ParseMAC(typeAndMAC)
if macErr == nil {
currentMAC = mac
}
}
case "lease":
leaseHex := strings.TrimPrefix(value, "0x")
parsed, parseErr := strconv.ParseInt(leaseHex, 16, 64)
if parseErr == nil {
currentLease = parsed
}
}
}
}

View File

@@ -1,224 +0,0 @@
//go:build linux
package route
import (
"net"
"net/netip"
"os"
"slices"
"sync"
"time"
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
var defaultLeaseFiles = []string{
"/tmp/dhcp.leases",
"/var/lib/dhcp/dhcpd.leases",
"/var/lib/dhcpd/dhcpd.leases",
"/var/lib/kea/kea-leases4.csv",
"/var/lib/kea/kea-leases6.csv",
}
type neighborResolver struct {
logger logger.ContextLogger
leaseFiles []string
access sync.RWMutex
neighborIPToMAC map[netip.Addr]net.HardwareAddr
leaseIPToMAC map[netip.Addr]net.HardwareAddr
ipToHostname map[netip.Addr]string
macToHostname map[string]string
watcher *fswatch.Watcher
done chan struct{}
}
func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) {
if len(leaseFiles) == 0 {
for _, path := range defaultLeaseFiles {
info, err := os.Stat(path)
if err == nil && info.Size() > 0 {
leaseFiles = append(leaseFiles, path)
}
}
}
return &neighborResolver{
logger: resolverLogger,
leaseFiles: leaseFiles,
neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr),
leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr),
ipToHostname: make(map[netip.Addr]string),
macToHostname: make(map[string]string),
done: make(chan struct{}),
}, nil
}
func (r *neighborResolver) Start() error {
err := r.loadNeighborTable()
if err != nil {
r.logger.Warn(E.Cause(err, "load neighbor table"))
}
r.doReloadLeaseFiles()
go r.subscribeNeighborUpdates()
if len(r.leaseFiles) > 0 {
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: r.leaseFiles,
Logger: r.logger,
Callback: func(_ string) {
r.doReloadLeaseFiles()
},
})
if err != nil {
r.logger.Warn(E.Cause(err, "create lease file watcher"))
} else {
r.watcher = watcher
err = watcher.Start()
if err != nil {
r.logger.Warn(E.Cause(err, "start lease file watcher"))
}
}
}
return nil
}
func (r *neighborResolver) Close() error {
close(r.done)
if r.watcher != nil {
return r.watcher.Close()
}
return nil
}
func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
r.access.RLock()
defer r.access.RUnlock()
mac, found := r.neighborIPToMAC[address]
if found {
return mac, true
}
mac, found = r.leaseIPToMAC[address]
if found {
return mac, true
}
mac, found = extractMACFromEUI64(address)
if found {
return mac, true
}
return nil, false
}
func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) {
r.access.RLock()
defer r.access.RUnlock()
hostname, found := r.ipToHostname[address]
if found {
return hostname, true
}
mac, macFound := r.neighborIPToMAC[address]
if !macFound {
mac, macFound = r.leaseIPToMAC[address]
}
if !macFound {
mac, macFound = extractMACFromEUI64(address)
}
if macFound {
hostname, found = r.macToHostname[mac.String()]
if found {
return hostname, true
}
}
return "", false
}
func (r *neighborResolver) loadNeighborTable() error {
connection, err := rtnetlink.Dial(nil)
if err != nil {
return E.Cause(err, "dial rtnetlink")
}
defer connection.Close()
neighbors, err := connection.Neigh.List()
if err != nil {
return E.Cause(err, "list neighbors")
}
r.access.Lock()
defer r.access.Unlock()
for _, neigh := range neighbors {
if neigh.Attributes == nil {
continue
}
if neigh.Attributes.LLAddress == nil || len(neigh.Attributes.Address) == 0 {
continue
}
address, ok := netip.AddrFromSlice(neigh.Attributes.Address)
if !ok {
continue
}
r.neighborIPToMAC[address] = slices.Clone(neigh.Attributes.LLAddress)
}
return nil
}
func (r *neighborResolver) subscribeNeighborUpdates() {
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
})
if err != nil {
r.logger.Warn(E.Cause(err, "subscribe neighbor updates"))
return
}
defer connection.Close()
for {
select {
case <-r.done:
return
default:
}
err = connection.SetReadDeadline(time.Now().Add(3 * time.Second))
if err != nil {
r.logger.Warn(E.Cause(err, "set netlink read deadline"))
return
}
messages, err := connection.Receive()
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
continue
}
select {
case <-r.done:
return
default:
}
r.logger.Warn(E.Cause(err, "receive neighbor update"))
continue
}
for _, message := range messages {
address, mac, isDelete, ok := ParseNeighborMessage(message)
if !ok {
continue
}
r.access.Lock()
if isDelete {
delete(r.neighborIPToMAC, address)
} else {
r.neighborIPToMAC[address] = mac
}
r.access.Unlock()
}
}
}
func (r *neighborResolver) doReloadLeaseFiles() {
leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles)
r.access.Lock()
r.leaseIPToMAC = leaseIPToMAC
r.ipToHostname = ipToHostname
r.macToHostname = macToHostname
r.access.Unlock()
}

View File

@@ -1,50 +0,0 @@
package route
import (
"encoding/binary"
"encoding/hex"
"net"
"net/netip"
"slices"
"strings"
)
func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) {
if len(duid) < 4 {
return nil, false
}
duidType := binary.BigEndian.Uint16(duid[0:2])
hwType := binary.BigEndian.Uint16(duid[2:4])
if hwType != 1 {
return nil, false
}
switch duidType {
case 1:
if len(duid) < 14 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[8:14])), true
case 3:
if len(duid) < 10 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[4:10])), true
}
return nil, false
}
func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) {
if !address.Is6() {
return nil, false
}
b := address.As16()
if b[11] != 0xff || b[12] != 0xfe {
return nil, false
}
return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true
}
func parseDUID(s string) ([]byte, error) {
cleaned := strings.ReplaceAll(s, ":", "")
return hex.DecodeString(cleaned)
}

View File

@@ -1,84 +0,0 @@
package route
import (
"net"
"net/netip"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
)
type platformNeighborResolver struct {
logger logger.ContextLogger
platform adapter.PlatformInterface
access sync.RWMutex
ipToMAC map[netip.Addr]net.HardwareAddr
ipToHostname map[netip.Addr]string
macToHostname map[string]string
}
func newPlatformNeighborResolver(resolverLogger logger.ContextLogger, platform adapter.PlatformInterface) adapter.NeighborResolver {
return &platformNeighborResolver{
logger: resolverLogger,
platform: platform,
ipToMAC: make(map[netip.Addr]net.HardwareAddr),
ipToHostname: make(map[netip.Addr]string),
macToHostname: make(map[string]string),
}
}
func (r *platformNeighborResolver) Start() error {
return r.platform.StartNeighborMonitor(r)
}
func (r *platformNeighborResolver) Close() error {
return r.platform.CloseNeighborMonitor(r)
}
func (r *platformNeighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
r.access.RLock()
defer r.access.RUnlock()
mac, found := r.ipToMAC[address]
if found {
return mac, true
}
return extractMACFromEUI64(address)
}
func (r *platformNeighborResolver) LookupHostname(address netip.Addr) (string, bool) {
r.access.RLock()
defer r.access.RUnlock()
hostname, found := r.ipToHostname[address]
if found {
return hostname, true
}
mac, found := r.ipToMAC[address]
if !found {
mac, found = extractMACFromEUI64(address)
}
if !found {
return "", false
}
hostname, found = r.macToHostname[mac.String()]
return hostname, found
}
func (r *platformNeighborResolver) UpdateNeighborTable(entries []adapter.NeighborEntry) {
ipToMAC := make(map[netip.Addr]net.HardwareAddr)
ipToHostname := make(map[netip.Addr]string)
macToHostname := make(map[string]string)
for _, entry := range entries {
ipToMAC[entry.Address] = entry.MACAddress
if entry.Hostname != "" {
ipToHostname[entry.Address] = entry.Hostname
macToHostname[entry.MACAddress.String()] = entry.Hostname
}
}
r.access.Lock()
r.ipToMAC = ipToMAC
r.ipToHostname = ipToHostname
r.macToHostname = macToHostname
r.access.Unlock()
r.logger.Info("updated neighbor table: ", len(entries), " entries")
}

View File

@@ -1,14 +0,0 @@
//go:build !linux && !darwin
package route
import (
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
)
func newNeighborResolver(_ logger.ContextLogger, _ []string) (adapter.NeighborResolver, error) {
return nil, os.ErrInvalid
}

View File

@@ -1,104 +0,0 @@
//go:build darwin
package route
import (
"net"
"net/netip"
"syscall"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"golang.org/x/net/route"
"golang.org/x/sys/unix"
)
func ReadNeighborEntries() ([]adapter.NeighborEntry, error) {
var entries []adapter.NeighborEntry
ipv4Entries, err := readNeighborEntriesAF(syscall.AF_INET)
if err != nil {
return nil, E.Cause(err, "read IPv4 neighbors")
}
entries = append(entries, ipv4Entries...)
ipv6Entries, err := readNeighborEntriesAF(syscall.AF_INET6)
if err != nil {
return nil, E.Cause(err, "read IPv6 neighbors")
}
entries = append(entries, ipv6Entries...)
return entries, nil
}
func readNeighborEntriesAF(addressFamily int) ([]adapter.NeighborEntry, error) {
rib, err := route.FetchRIB(addressFamily, route.RIBType(syscall.NET_RT_FLAGS), syscall.RTF_LLINFO)
if err != nil {
return nil, err
}
messages, err := route.ParseRIB(route.RIBType(syscall.NET_RT_FLAGS), rib)
if err != nil {
return nil, err
}
var entries []adapter.NeighborEntry
for _, message := range messages {
routeMessage, isRouteMessage := message.(*route.RouteMessage)
if !isRouteMessage {
continue
}
address, macAddress, ok := parseRouteNeighborEntry(routeMessage)
if !ok {
continue
}
entries = append(entries, adapter.NeighborEntry{
Address: address,
MACAddress: macAddress,
})
}
return entries, nil
}
func parseRouteNeighborEntry(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, ok bool) {
if len(message.Addrs) <= unix.RTAX_GATEWAY {
return
}
gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr)
if !isLinkAddr || len(gateway.Addr) < 6 {
return
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
address = netip.AddrFrom4(destination.IP)
case *route.Inet6Addr:
address = netip.AddrFrom16(destination.IP)
default:
return
}
macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr)))
copy(macAddress, gateway.Addr)
ok = true
return
}
func ParseRouteNeighborMessage(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) {
isDelete = message.Type == unix.RTM_DELETE
if len(message.Addrs) <= unix.RTAX_GATEWAY {
return
}
switch destination := message.Addrs[unix.RTAX_DST].(type) {
case *route.Inet4Addr:
address = netip.AddrFrom4(destination.IP)
case *route.Inet6Addr:
address = netip.AddrFrom16(destination.IP)
default:
return
}
if !isDelete {
gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr)
if !isLinkAddr || len(gateway.Addr) < 6 {
return
}
macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr)))
copy(macAddress, gateway.Addr)
}
ok = true
return
}

View File

@@ -1,68 +0,0 @@
//go:build linux
package route
import (
"net"
"net/netip"
"slices"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
func ReadNeighborEntries() ([]adapter.NeighborEntry, error) {
connection, err := rtnetlink.Dial(nil)
if err != nil {
return nil, E.Cause(err, "dial rtnetlink")
}
defer connection.Close()
neighbors, err := connection.Neigh.List()
if err != nil {
return nil, E.Cause(err, "list neighbors")
}
var entries []adapter.NeighborEntry
for _, neighbor := range neighbors {
if neighbor.Attributes == nil {
continue
}
if neighbor.Attributes.LLAddress == nil || len(neighbor.Attributes.Address) == 0 {
continue
}
address, ok := netip.AddrFromSlice(neighbor.Attributes.Address)
if !ok {
continue
}
entries = append(entries, adapter.NeighborEntry{
Address: address,
MACAddress: slices.Clone(neighbor.Attributes.LLAddress),
})
}
return entries, nil
}
func ParseNeighborMessage(message netlink.Message) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) {
var neighMessage rtnetlink.NeighMessage
err := neighMessage.UnmarshalBinary(message.Data)
if err != nil {
return
}
if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 {
return
}
address, ok = netip.AddrFromSlice(neighMessage.Attributes.Address)
if !ok {
return
}
isDelete = message.Header.Type == unix.RTM_DELNEIGH
if !isDelete && neighMessage.Attributes.LLAddress == nil {
ok = false
return
}
macAddress = slices.Clone(neighMessage.Attributes.LLAddress)
return
}

View File

@@ -438,23 +438,6 @@ func (r *Router) matchRule(
metadata.ProcessInfo = processInfo
}
}
if r.neighborResolver != nil && metadata.SourceMACAddress == nil && metadata.Source.Addr.IsValid() {
mac, macFound := r.neighborResolver.LookupMAC(metadata.Source.Addr)
if macFound {
metadata.SourceMACAddress = mac
}
hostname, hostnameFound := r.neighborResolver.LookupHostname(metadata.Source.Addr)
if hostnameFound {
metadata.SourceHostname = hostname
if macFound {
r.logger.InfoContext(ctx, "found neighbor: ", mac, ", hostname: ", hostname)
} else {
r.logger.InfoContext(ctx, "found neighbor hostname: ", hostname)
}
} else if macFound {
r.logger.InfoContext(ctx, "found neighbor: ", mac)
}
}
if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) {
domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr)
if !loaded {

View File

@@ -35,13 +35,10 @@ type Router struct {
network adapter.NetworkManager
rules []adapter.Rule
needFindProcess bool
needFindNeighbor bool
leaseFiles []string
ruleSets []adapter.RuleSet
ruleSetMap map[string]adapter.RuleSet
processSearcher process.Searcher
processCache freelru.Cache[processCacheKey, processCacheEntry]
neighborResolver adapter.NeighborResolver
pauseManager pause.Manager
trackers []adapter.ConnectionTracker
platformInterface adapter.PlatformInterface
@@ -61,8 +58,6 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
rules: make([]adapter.Rule, 0, len(options.Rules)),
ruleSetMap: make(map[string]adapter.RuleSet),
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor,
leaseFiles: options.DHCPLeaseFiles,
pauseManager: service.FromContext[pause.Manager](ctx),
platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
}
@@ -122,7 +117,6 @@ func (r *Router) Start(stage adapter.StartStage) error {
}
r.network.Initialize(r.ruleSets)
needFindProcess := r.needFindProcess
needFindNeighbor := r.needFindNeighbor
for _, ruleSet := range r.ruleSets {
metadata := ruleSet.Metadata()
if metadata.ContainsProcessRule {
@@ -157,36 +151,6 @@ func (r *Router) Start(stage adapter.StartStage) error {
processCache.SetLifetime(200 * time.Millisecond)
r.processCache = processCache
}
r.needFindNeighbor = needFindNeighbor
if needFindNeighbor {
if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() {
monitor.Start("initialize neighbor resolver")
resolver := newPlatformNeighborResolver(r.logger, r.platformInterface)
err := resolver.Start()
monitor.Finish()
if err != nil {
r.logger.Error(E.Cause(err, "start neighbor resolver"))
} else {
r.neighborResolver = resolver
}
} else {
monitor.Start("initialize neighbor resolver")
resolver, err := newNeighborResolver(r.logger, r.leaseFiles)
monitor.Finish()
if err != nil {
if err != os.ErrInvalid {
r.logger.Error(E.Cause(err, "create neighbor resolver"))
}
} else {
err = resolver.Start()
if err != nil {
r.logger.Error(E.Cause(err, "start neighbor resolver"))
} else {
r.neighborResolver = resolver
}
}
}
}
case adapter.StartStatePostStart:
for i, rule := range r.rules {
monitor.Start("initialize rule[", i, "]")
@@ -218,13 +182,6 @@ func (r *Router) Start(stage adapter.StartStage) error {
func (r *Router) Close() error {
monitor := taskmonitor.New(r.logger, C.StopTimeout)
var err error
if r.neighborResolver != nil {
monitor.Start("close neighbor resolver")
err = E.Append(err, r.neighborResolver.Close(), func(closeErr error) error {
return E.Cause(closeErr, "close neighbor resolver")
})
monitor.Finish()
}
for i, rule := range r.rules {
monitor.Start("close rule[", i, "]")
err = E.Append(err, rule.Close(), func(err error) error {
@@ -266,14 +223,6 @@ func (r *Router) NeedFindProcess() bool {
return r.needFindProcess
}
func (r *Router) NeedFindNeighbor() bool {
return r.needFindNeighbor
}
func (r *Router) NeighborResolver() adapter.NeighborResolver {
return r.neighborResolver
}
func (r *Router) ResetNetwork() {
r.network.ResetNetwork()
r.dns.ResetNetwork()

View File

@@ -264,16 +264,6 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceMACAddress) > 0 {
item := NewSourceMACAddressItem(options.SourceMACAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceHostname) > 0 {
item := NewSourceHostnameItem(options.SourceHostname)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PreferredBy) > 0 {
item := NewPreferredByItem(ctx, options.PreferredBy)
rule.items = append(rule.items, item)

View File

@@ -265,16 +265,6 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceMACAddress) > 0 {
item := NewSourceMACAddressItem(options.SourceMACAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceHostname) > 0 {
item := NewSourceHostnameItem(options.SourceHostname)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.RuleSet) > 0 {
//nolint:staticcheck
if options.Deprecated_RulesetIPCIDRMatchSource {

View File

@@ -1,42 +0,0 @@
package rule
import (
"strings"
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*SourceHostnameItem)(nil)
type SourceHostnameItem struct {
hostnames []string
hostnameMap map[string]bool
}
func NewSourceHostnameItem(hostnameList []string) *SourceHostnameItem {
rule := &SourceHostnameItem{
hostnames: hostnameList,
hostnameMap: make(map[string]bool),
}
for _, hostname := range hostnameList {
rule.hostnameMap[hostname] = true
}
return rule
}
func (r *SourceHostnameItem) Match(metadata *adapter.InboundContext) bool {
if metadata.SourceHostname == "" {
return false
}
return r.hostnameMap[metadata.SourceHostname]
}
func (r *SourceHostnameItem) String() string {
var description string
if len(r.hostnames) == 1 {
description = "source_hostname=" + r.hostnames[0]
} else {
description = "source_hostname=[" + strings.Join(r.hostnames, " ") + "]"
}
return description
}

View File

@@ -1,48 +0,0 @@
package rule
import (
"net"
"strings"
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*SourceMACAddressItem)(nil)
type SourceMACAddressItem struct {
addresses []string
addressMap map[string]bool
}
func NewSourceMACAddressItem(addressList []string) *SourceMACAddressItem {
rule := &SourceMACAddressItem{
addresses: addressList,
addressMap: make(map[string]bool),
}
for _, address := range addressList {
parsed, err := net.ParseMAC(address)
if err == nil {
rule.addressMap[parsed.String()] = true
} else {
rule.addressMap[address] = true
}
}
return rule
}
func (r *SourceMACAddressItem) Match(metadata *adapter.InboundContext) bool {
if metadata.SourceMACAddress == nil {
return false
}
return r.addressMap[metadata.SourceMACAddress.String()]
}
func (r *SourceMACAddressItem) String() string {
var description string
if len(r.addresses) == 1 {
description = "source_mac_address=" + r.addresses[0]
} else {
description = "source_mac_address=[" + strings.Join(r.addresses, " ") + "]"
}
return description
}

View File

@@ -45,14 +45,6 @@ func isProcessDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isNeighborRule(rule option.DefaultRule) bool {
return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0
}
func isNeighborDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0
}
func isWIFIRule(rule option.DefaultRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}

View File

@@ -1,411 +0,0 @@
//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
}

Some files were not shown because too many files have changed in this diff Show More