From 3aae100ea9293d4657e62c7c44021868c3cf8754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 28 Mar 2025 22:42:59 +0800 Subject: [PATCH] Add DERP service --- adapter/dns.go | 20 ++ adapter/fakeip.go | 2 +- adapter/lifecycle.go | 5 + adapter/lifecycle_legacy.go | 12 +- adapter/rule.go | 2 +- adapter/service.go | 25 +- adapter/service/adapter.go | 21 ++ adapter/service/manager.go | 143 ++++++++++ adapter/service/registry.go | 72 +++++ adapter/time.go | 2 +- box.go | 115 +++++--- cmd/sing-box/cmd.go | 2 +- common/dialer/dialer.go | 4 +- common/tls/acme.go | 2 +- common/tls/acme_stub.go | 2 +- common/tls/std_server.go | 4 +- constant/proxy.go | 2 + experimental/libbox/config.go | 2 +- go.mod | 6 +- go.sum | 19 +- include/registry.go | 9 + include/tailscale.go | 7 + include/tailscale_stub.go | 10 + option/dns.go | 1 - option/endpoint.go | 4 +- option/inbound.go | 2 +- option/options.go | 1 + option/service.go | 47 ++++ option/tailscale.go | 62 +++++ protocol/tailscale/endpoint.go | 30 ++- service/derp/derp.go | 463 +++++++++++++++++++++++++++++++++ service/derp/stun.go | 89 +++++++ 32 files changed, 1105 insertions(+), 82 deletions(-) create mode 100644 adapter/service/adapter.go create mode 100644 adapter/service/manager.go create mode 100644 adapter/service/registry.go create mode 100644 option/service.go create mode 100644 service/derp/derp.go create mode 100644 service/derp/stun.go diff --git a/adapter/dns.go b/adapter/dns.go index 942f3566..73b77601 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -7,7 +7,9 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" "github.com/miekg/dns" ) @@ -38,6 +40,24 @@ type DNSQueryOptions struct { ClientSubnet netip.Prefix } +func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { + if options == nil { + return &DNSQueryOptions{}, nil + } + transportManager := service.FromContext[DNSTransportManager](ctx) + transport, loaded := transportManager.Transport(options.Server) + if !loaded { + return nil, E.New("domain resolver not found: " + options.Server) + } + return &DNSQueryOptions{ + Transport: transport, + Strategy: C.DomainStrategy(options.Strategy), + DisableCache: options.DisableCache, + RewriteTTL: options.RewriteTTL, + ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), + }, nil +} + type RDRCStore interface { LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) SaveRDRC(transportName string, qName string, qType uint16) error diff --git a/adapter/fakeip.go b/adapter/fakeip.go index 97d1c3c0..0787c146 100644 --- a/adapter/fakeip.go +++ b/adapter/fakeip.go @@ -7,7 +7,7 @@ import ( ) type FakeIPStore interface { - Service + SimpleLifecycle Contains(address netip.Addr) bool Create(domain string, isIPv6 bool) (netip.Addr, error) Lookup(address netip.Addr) (string, bool) diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go index aff9fadb..face00b7 100644 --- a/adapter/lifecycle.go +++ b/adapter/lifecycle.go @@ -2,6 +2,11 @@ package adapter import E "github.com/sagernet/sing/common/exceptions" +type SimpleLifecycle interface { + Start() error + Close() error +} + type StartStage uint8 const ( diff --git a/adapter/lifecycle_legacy.go b/adapter/lifecycle_legacy.go index 94a5cf8c..f8b25db6 100644 --- a/adapter/lifecycle_legacy.go +++ b/adapter/lifecycle_legacy.go @@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error { } type lifecycleServiceWrapper struct { - Service + SimpleLifecycle name string } -func NewLifecycleService(service Service, name string) LifecycleService { +func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService { return &lifecycleServiceWrapper{ - Service: service, - name: name, + SimpleLifecycle: service, + name: name, } } @@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string { } func (l *lifecycleServiceWrapper) Start(stage StartStage) error { - return LegacyStart(l.Service, stage) + return LegacyStart(l.SimpleLifecycle, stage) } func (l *lifecycleServiceWrapper) Close() error { - return l.Service.Close() + return l.SimpleLifecycle.Close() } diff --git a/adapter/rule.go b/adapter/rule.go index 2512a77b..f8ee797d 100644 --- a/adapter/rule.go +++ b/adapter/rule.go @@ -11,7 +11,7 @@ type HeadlessRule interface { type Rule interface { HeadlessRule - Service + SimpleLifecycle Type() string Action() RuleAction } diff --git a/adapter/service.go b/adapter/service.go index 5ed0798d..534bd7eb 100644 --- a/adapter/service.go +++ b/adapter/service.go @@ -1,6 +1,27 @@ package adapter +import ( + "context" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + type Service interface { - Start() error - Close() error + Lifecycle + Type() string + Tag() string +} + +type ServiceRegistry interface { + option.ServiceOptionsRegistry + Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error) +} + +type ServiceManager interface { + Lifecycle + Services() []Service + Get(tag string) (Service, bool) + Remove(tag string) error + Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error } diff --git a/adapter/service/adapter.go b/adapter/service/adapter.go new file mode 100644 index 00000000..6c6242ea --- /dev/null +++ b/adapter/service/adapter.go @@ -0,0 +1,21 @@ +package service + +type Adapter struct { + serviceType string + serviceTag string +} + +func NewAdapter(serviceType string, serviceTag string) Adapter { + return Adapter{ + serviceType: serviceType, + serviceTag: serviceTag, + } +} + +func (a *Adapter) Type() string { + return a.serviceType +} + +func (a *Adapter) Tag() string { + return a.serviceTag +} diff --git a/adapter/service/manager.go b/adapter/service/manager.go new file mode 100644 index 00000000..3ef787cb --- /dev/null +++ b/adapter/service/manager.go @@ -0,0 +1,143 @@ +package service + +import ( + "context" + "os" + "sync" + + "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" +) + +var _ adapter.ServiceManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.ServiceRegistry + access sync.Mutex + started bool + stage adapter.StartStage + services []adapter.Service + serviceByTag map[string]adapter.Service +} + +func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + serviceByTag: make(map[string]adapter.Service), + } +} + +func (m *Manager) Start(stage adapter.StartStage) error { + m.access.Lock() + defer m.access.Unlock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + for _, service := range m.services { + err := adapter.LegacyStart(service, stage) + if err != nil { + return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]") + } + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + services := m.services + m.services = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, service := range services { + monitor.Start("close service/", service.Type(), "[", service.Tag(), "]") + err = E.Append(err, service.Close(), func(err error) error { + return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]") + }) + monitor.Finish() + } + return nil +} + +func (m *Manager) Services() []adapter.Service { + m.access.Lock() + defer m.access.Unlock() + return m.services +} + +func (m *Manager) Get(tag string) (adapter.Service, bool) { + m.access.Lock() + service, found := m.serviceByTag[tag] + m.access.Unlock() + return service, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + service, found := m.serviceByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.serviceByTag, tag) + index := common.Index(m.services, func(it adapter.Service) bool { + return it == service + }) + if index == -1 { + panic("invalid service index") + } + m.services = append(m.services[:index], m.services[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return service.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error { + service, err := m.registry.Create(ctx, logger, tag, serviceType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + for _, stage := range adapter.ListStartStages { + err = adapter.LegacyStart(service, stage) + if err != nil { + return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]") + } + } + } + if existsService, loaded := m.serviceByTag[tag]; loaded { + if m.started { + err = existsService.Close() + if err != nil { + return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]") + } + } + existsIndex := common.Index(m.services, func(it adapter.Service) bool { + return it == existsService + }) + if existsIndex == -1 { + panic("invalid service index") + } + m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...) + } + m.services = append(m.services, service) + m.serviceByTag[tag] = service + return nil +} diff --git a/adapter/service/registry.go b/adapter/service/registry.go new file mode 100644 index 00000000..42fec82f --- /dev/null +++ b/adapter/service/registry.go @@ -0,0 +1,72 @@ +package service + +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.Service, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.ServiceRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, 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(outboundType string) (any, bool) { + m.access.Lock() + defer m.access.Unlock() + optionsConstructor, loaded := m.optionsType[outboundType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) { + m.access.Lock() + defer m.access.Unlock() + constructor, loaded := m.constructor[outboundType] + if !loaded { + return nil, E.New("outbound type not found: " + outboundType) + } + return constructor(ctx, logger, tag, options) +} + +func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + m.access.Lock() + defer m.access.Unlock() + m.optionsType[outboundType] = optionsConstructor + m.constructor[outboundType] = constructor +} diff --git a/adapter/time.go b/adapter/time.go index 3cb13fc1..be2631d8 100644 --- a/adapter/time.go +++ b/adapter/time.go @@ -3,6 +3,6 @@ package adapter import "time" type TimeService interface { - Service + SimpleLifecycle TimeFunc() func() time.Time } diff --git a/box.go b/box.go index 0f176474..d1f34007 100644 --- a/box.go +++ b/box.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" + boxService "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/taskmonitor" @@ -34,22 +35,23 @@ import ( "github.com/sagernet/sing/service/pause" ) -var _ adapter.Service = (*Box)(nil) +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 - dnsTransport *dns.TransportManager - dnsRouter *dns.Router - connection *route.ConnectionManager - router *route.Router - services []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 { @@ -64,6 +66,7 @@ func Context( outboundRegistry adapter.OutboundRegistry, endpointRegistry adapter.EndpointRegistry, dnsTransportRegistry adapter.DNSTransportRegistry, + serviceRegistry adapter.ServiceRegistry, ) context.Context { if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || service.FromContext[adapter.InboundRegistry](ctx) == nil { @@ -84,6 +87,10 @@ func Context( ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) } + if service.FromContext[adapter.ServiceRegistry](ctx) == nil { + ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry) + ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry) + } return ctx } @@ -99,6 +106,7 @@ func New(options Options) (*Box, error) { inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) + serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx) if endpointRegistry == nil { return nil, E.New("missing endpoint registry in context") @@ -109,6 +117,12 @@ func New(options Options) (*Box, error) { if outboundRegistry == nil { return nil, E.New("missing outbound registry in context") } + if dnsTransportRegistry == nil { + return nil, E.New("missing DNS transport registry in context") + } + if serviceRegistry == nil { + return nil, E.New("missing service registry in context") + } ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) @@ -142,7 +156,7 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create log factory") } - var services []adapter.LifecycleService + var internalServices []adapter.LifecycleService certificateOptions := common.PtrValueOrDefault(options.Certificate) if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || len(certificateOptions.Certificate) > 0 || @@ -153,7 +167,7 @@ func New(options Options) (*Box, error) { return nil, err } service.MustRegister[adapter.CertificateStore](ctx, certificateStore) - services = append(services, certificateStore) + internalServices = append(internalServices, certificateStore) } routeOptions := common.PtrValueOrDefault(options.Route) @@ -162,6 +176,7 @@ func New(options Options) (*Box, error) { inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) 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) service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager) @@ -280,6 +295,24 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize outbound[", i, "]") } } + for i, serviceOptions := range options.Services { + var tag string + if serviceOptions.Tag != "" { + tag = serviceOptions.Tag + } else { + tag = F.ToString(i) + } + err = serviceManager.Create( + ctx, + logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")), + tag, + serviceOptions.Type, + serviceOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize service[", i, "]") + } + } outboundManager.Initialize(common.Must1( direct.NewOutbound( ctx, @@ -305,7 +338,7 @@ func New(options Options) (*Box, error) { if needCacheFile { cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) service.MustRegister[adapter.CacheFile](ctx, cacheFile) - services = append(services, cacheFile) + internalServices = append(internalServices, cacheFile) } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) @@ -316,7 +349,7 @@ func New(options Options) (*Box, error) { } router.SetTracker(clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer) - services = append(services, clashServer) + internalServices = append(internalServices, clashServer) } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) @@ -325,7 +358,7 @@ func New(options Options) (*Box, error) { } if v2rayServer.StatsService() != nil { router.SetTracker(v2rayServer.StatsService()) - services = append(services, v2rayServer) + internalServices = append(internalServices, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) } } @@ -343,22 +376,22 @@ func New(options Options) (*Box, error) { WriteToSystem: ntpOptions.WriteToSystem, }) timeService.TimeService = ntpService - services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) + internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service")) } return &Box{ - network: networkManager, - endpoint: endpointManager, - inbound: inboundManager, - outbound: outboundManager, - dnsTransport: dnsTransportManager, - dnsRouter: dnsRouter, - connection: connectionManager, - router: router, - createdAt: createdAt, - logFactory: logFactory, - logger: logFactory.Logger(), - services: services, - done: make(chan struct{}), + network: networkManager, + endpoint: endpointManager, + inbound: inboundManager, + outbound: outboundManager, + dnsTransport: dnsTransportManager, + dnsRouter: dnsRouter, + connection: connectionManager, + router: router, + createdAt: createdAt, + logFactory: logFactory, + logger: logFactory.Logger(), + internalService: internalServices, + done: make(chan struct{}), }, nil } @@ -408,11 +441,11 @@ func (s *Box) preStart() error { if err != nil { return E.Cause(err, "start logger") } - err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api + err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api if err != nil { return err } - err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) + err = adapter.Start(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 } @@ -428,7 +461,7 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.StartNamed(adapter.StartStateStart, s.services) + err = adapter.StartNamed(adapter.StartStateStart, s.internalService) if err != nil { return err } @@ -440,19 +473,19 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint) + err = adapter.Start(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 } - err = adapter.StartNamed(adapter.StartStatePostStart, s.services) + err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService) if err != nil { return err } - err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) + err = adapter.Start(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 } - err = adapter.StartNamed(adapter.StartStateStarted, s.services) + err = adapter.StartNamed(adapter.StartStateStarted, s.internalService) if err != nil { return err } @@ -469,7 +502,7 @@ func (s *Box) Close() error { err := common.Close( s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, ) - for _, lifecycleService := range s.services { + for _, lifecycleService := range s.internalService { err = E.Append(err, lifecycleService.Close(), func(err error) error { return E.Cause(err, "close ", lifecycleService.Name()) }) diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go index 55fe1179..78b55a6f 100644 --- a/cmd/sing-box/cmd.go +++ b/cmd/sing-box/cmd.go @@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) { configPaths = append(configPaths, "config.json") } globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) - globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry()) + globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry()) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 88e16740..bfd13784 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -102,12 +102,12 @@ func NewWithOptions(options Options) (N.Dialer, error) { } dnsQueryOptions.Transport = transport resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) - } else if options.NewDialer { - return nil, E.New("missing domain resolver for domain server address") } else { transports := dnsTransport.Transports() if len(transports) < 2 { dnsQueryOptions.Transport = dnsTransport.Default() + } else if options.NewDialer { + return nil, E.New("missing domain resolver for domain server address") } else { deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) } diff --git a/common/tls/acme.go b/common/tls/acme.go index 3f251473..18734f41 100644 --- a/common/tls/acme.go +++ b/common/tls/acme.go @@ -37,7 +37,7 @@ func (w *acmeWrapper) Close() error { return nil } -func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { +func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { var acmeServer string switch options.Provider { case "", "letsencrypt": diff --git a/common/tls/acme_stub.go b/common/tls/acme_stub.go index d97d0540..253df126 100644 --- a/common/tls/acme_stub.go +++ b/common/tls/acme_stub.go @@ -11,6 +11,6 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) -func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { +func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) { return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 04b7cfea..5bd365d2 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -21,7 +21,7 @@ var errInsecureUnused = E.New("tls: insecure unused") type STDServerConfig struct { config *tls.Config logger log.Logger - acmeService adapter.Service + acmeService adapter.SimpleLifecycle certificate []byte key []byte certificatePath string @@ -164,7 +164,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound return nil, nil } var tlsConfig *tls.Config - var acmeService adapter.Service + var acmeService adapter.SimpleLifecycle var err error if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck diff --git a/constant/proxy.go b/constant/proxy.go index 1044428b..40112d6e 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -25,6 +25,8 @@ const ( TypeTUIC = "tuic" TypeHysteria2 = "hysteria2" TypeTailscale = "tailscale" + TypeDERP = "derp" + TypeDERPSTUN = "derp-stun" ) const ( diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index d3149dc2..60bb1286 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -33,7 +33,7 @@ func BaseContext(platformInterface PlatformInterface) context.Context { }) } } - return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry) + return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry()) } func parseConfig(ctx context.Context, configContent string) (option.Options, error) { diff --git a/go.mod b/go.mod index d5bd0b0c..cf11a0ff 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/anytls/sing-anytls v0.0.7 github.com/caddyserver/certmagic v0.21.7 github.com/cloudflare/circl v1.6.0 + github.com/coder/websocket v1.8.12 github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/render v1.0.3 @@ -35,7 +36,7 @@ require ( github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec github.com/sagernet/sing-vmess v0.2.0 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 - github.com/sagernet/tailscale v1.80.3-mod.0 + github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328134432-aa6f5b2b14e3 github.com/sagernet/utls v1.6.7 github.com/sagernet/wireguard-go v0.0.1-beta.5 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 @@ -66,7 +67,6 @@ require ( github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect @@ -98,6 +98,7 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect @@ -136,6 +137,7 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 1c20cdaf..838101c5 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,7 @@ github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3C github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -110,6 +111,13 @@ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kK github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= @@ -145,6 +153,7 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5 github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -154,6 +163,9 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= @@ -196,8 +208,8 @@ github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGT github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/tailscale v1.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508= -github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= +github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328134432-aa6f5b2b14e3 h1:ZptrQ12SRtLlQUAmO2Vz47w8WhLJA/VVc9IbUSFFuNo= +github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328134432-aa6f5b2b14e3/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc= @@ -318,8 +330,9 @@ google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/include/registry.go b/include/registry.go index 9be1f2b4..65e6db7b 100644 --- a/include/registry.go +++ b/include/registry.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/adapter/service" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns/transport" @@ -118,6 +119,14 @@ func DNSTransportRegistry() *dns.TransportRegistry { return registry } +func ServiceRegistry() *service.Registry { + registry := service.NewRegistry() + + registerDERPService(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") diff --git a/include/tailscale.go b/include/tailscale.go index 05eed2cd..8d3847f5 100644 --- a/include/tailscale.go +++ b/include/tailscale.go @@ -4,8 +4,10 @@ package include import ( "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/service" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/protocol/tailscale" + "github.com/sagernet/sing-box/service/derp" ) func registerTailscaleEndpoint(registry *endpoint.Registry) { @@ -15,3 +17,8 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) { func registerTailscaleTransport(registry *dns.TransportRegistry) { tailscale.RegistryTransport(registry) } + +func registerDERPService(registry *service.Registry) { + derp.Register(registry) + derp.RegisterSTUN(registry) +} diff --git a/include/tailscale_stub.go b/include/tailscale_stub.go index ddf6485e..d6182007 100644 --- a/include/tailscale_stub.go +++ b/include/tailscale_stub.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/service" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" @@ -25,3 +26,12 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) { 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`) + }) + service.Register[option.DERPSTUNServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPSTUNServiceOptions) (adapter.Service, error) { + return nil, E.New(`STUN (DERP) is not included in this build, rebuild with -tags with_tailscale`) + }) +} diff --git a/option/dns.go b/option/dns.go index f303b894..37445782 100644 --- a/option/dns.go +++ b/option/dns.go @@ -121,7 +121,6 @@ type LegacyDNSFakeIPOptions struct { type DNSTransportOptionsRegistry interface { CreateOptions(transportType string) (any, bool) } - type _DNSServerOptions struct { Type string `json:"type,omitempty"` Tag string `json:"tag,omitempty"` diff --git a/option/endpoint.go b/option/endpoint.go index 909fb896..45c4f831 100644 --- a/option/endpoint.go +++ b/option/endpoint.go @@ -32,11 +32,11 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err } registry := service.FromContext[EndpointOptionsRegistry](ctx) if registry == nil { - return E.New("missing Endpoint fields registry in context") + return E.New("missing endpoint fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { - return E.New("unknown inbound type: ", h.Type) + return E.New("unknown endpoint type: ", h.Type) } err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options) if err != nil { diff --git a/option/inbound.go b/option/inbound.go index b704c7e3..3b6c8d9b 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro } registry := service.FromContext[InboundOptionsRegistry](ctx) if registry == nil { - return E.New("missing Inbound fields registry in context") + return E.New("missing inbound fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { diff --git a/option/options.go b/option/options.go index 168074ed..d936554b 100644 --- a/option/options.go +++ b/option/options.go @@ -18,6 +18,7 @@ type _Options struct { 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"` } diff --git a/option/service.go b/option/service.go new file mode 100644 index 00000000..7d45bc14 --- /dev/null +++ b/option/service.go @@ -0,0 +1,47 @@ +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 ServiceOptionsRegistry interface { + CreateOptions(serviceType string) (any, bool) +} + +type _Service struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type Service _Service + +func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options) +} + +func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_Service)(h)) + if err != nil { + return err + } + registry := service.FromContext[ServiceOptionsRegistry](ctx) + if registry == nil { + return E.New("missing service fields registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown inbound type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options) + if err != nil { + return err + } + h.Options = options + return nil +} diff --git a/option/tailscale.go b/option/tailscale.go index 30579fc7..1e201c29 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -2,6 +2,12 @@ package option import ( "net/netip" + "net/url" + "reflect" + + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" + M "github.com/sagernet/sing/common/metadata" ) type TailscaleEndpointOptions struct { @@ -22,3 +28,59 @@ type TailscaleDNSServerOptions struct { Endpoint string `json:"endpoint,omitempty"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` } + +type DERPServiceOptions struct { + ListenOptions + InboundTLSOptionsContainer + ConfigPath string `json:"config_path,omitempty"` + VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"` + VerifyClientURL badoption.Listable[DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"` + MeshWith badoption.Listable[DERPMeshOptions] `json:"mesh_with,omitempty"` + MeshPSK string `json:"mesh_psk,omitempty"` + MeshPSKFile string `json:"mesh_psk_file,omitempty"` + DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"` +} + +type _DERPVerifyClientURLOptions struct { + URL string `json:"url,omitempty"` + DialerOptions +} + +type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions + +func (d DERPVerifyClientURLOptions) ServerIsDomain() bool { + verifyURL, err := url.Parse(d.URL) + if err != nil { + return false + } + return M.IsDomainName(verifyURL.Host) +} + +func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) { + if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) { + return json.Marshal(d.URL) + } else { + return json.Marshal(_DERPVerifyClientURLOptions(d)) + } +} + +func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error { + var stringValue string + err := json.Unmarshal(bytes, &stringValue) + if err == nil { + d.URL = stringValue + return nil + } + return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d)) +} + +type DERPMeshOptions struct { + ServerOptions + Host string `json:"host,omitempty"` + OutboundTLSOptionsContainer + DialerOptions +} + +type DERPSTUNServiceOptions struct { + ListenOptions +} diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 5a0731f7..136d43d5 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -148,7 +148,19 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) }, - DNS: &dnsConfigurtor{}, + OnlyTCP443: options.Detour != "", + DNS: &dnsConfigurtor{}, + HTTPClient: &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address)) + }, + TLSClientConfig: &tls.Config{ + RootCAs: adapter.RootPoolFromContext(ctx), + }, + }, + }, } return &Endpoint{ Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), @@ -214,18 +226,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { t.stack = ipStack localBackend := t.server.ExportLocalBackend() - localBackend.SetHTTPTestClient(&http.Client{ - Transport: &http.Transport{ - ForceAttemptHTTP2: true, - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - return t.server.Dialer.DialContext(ctx, network, M.ParseSocksaddr(address)) - }, - TLSClientConfig: &tls.Config{ - RootCAs: adapter.RootPoolFromContext(t.ctx), - }, - }, - }) - perfs := &ipn.MaskedPrefs{ ExitNodeIPSet: true, AdvertiseRoutesSet: true, @@ -460,6 +460,10 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } +func (t *Endpoint) Server() *tsnet.Server { + return t.server +} + func addressFromAddr(destination netip.Addr) tcpip.Address { if destination.Is6() { return tcpip.AddrFrom16(destination.As16()) diff --git a/service/derp/derp.go b/service/derp/derp.go new file mode 100644 index 00000000..48cf374d --- /dev/null +++ b/service/derp/derp.go @@ -0,0 +1,463 @@ +package derp + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + "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" + boxScale "github.com/sagernet/sing-box/protocol/tailscale" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + "github.com/sagernet/tailscale/client/tailscale" + "github.com/sagernet/tailscale/derp" + "github.com/sagernet/tailscale/derp/derphttp" + "github.com/sagernet/tailscale/net/netmon" + "github.com/sagernet/tailscale/net/wsconn" + "github.com/sagernet/tailscale/tsweb" + "github.com/sagernet/tailscale/types/key" + + "github.com/coder/websocket" + "github.com/go-chi/render" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func Register(registry *boxService.Registry) { + boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService) +} + +type Service struct { + boxService.Adapter + ctx context.Context + logger logger.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + server *derp.Server + configPath string + verifyClientEndpoint []string + verifyClientURL []option.DERPVerifyClientURLOptions + home string + domainResolveOptions *option.DomainResolveOptions + domainResolver *adapter.DNSQueryOptions + meshKey string + meshKeyPath string + meshWith []option.DERPMeshOptions +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) { + if options.TLS == nil || !options.TLS.Enabled { + return nil, E.New("TLS is required for DERP server") + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + + var configPath string + if options.ConfigPath != "" { + configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath)) + } else if os.Getuid() == 0 { + configPath = "/var/lib/derper/derper.key" + } else { + return nil, E.New("missing config_path") + } + + if options.MeshPSK != "" { + err = checkMeshKey(options.MeshPSK) + if err != nil { + return nil, E.Cause(err, "invalid mesh_psk") + } + } + + return &Service{ + Adapter: boxService.NewAdapter(C.TypeDERP, tag), + ctx: ctx, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + tlsConfig: tlsConfig, + configPath: configPath, + verifyClientEndpoint: options.VerifyClientEndpoint, + verifyClientURL: options.VerifyClientURL, + meshKey: options.MeshPSK, + meshKeyPath: options.MeshPSKFile, + meshWith: options.MeshWith, + domainResolveOptions: options.DomainResolver, + }, nil +} + +func (d *Service) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateInitialize: + domainResolver, err := adapter.DNSQueryOptionsFrom(d.ctx, d.domainResolveOptions) + if err != nil { + return err + } + d.domainResolver = domainResolver + case adapter.StartStateStart: + config, err := readDERPConfig(d.configPath) + if err != nil { + return err + } + + server := derp.NewServer(config.PrivateKey, func(format string, args ...any) { + d.logger.Debug(fmt.Sprintf(format, args...)) + }) + + if len(d.verifyClientURL) > 0 { + var httpClients []*http.Client + var urls []string + for index, options := range d.verifyClientURL { + verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{ + Context: d.ctx, + Options: options.DialerOptions, + RemoteIsDomain: options.ServerIsDomain(), + }) + if createErr != nil { + return E.Cause(createErr, "verify_client_url[", index, "]") + } + httpClients = append(httpClients, &http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + }) + urls = append(urls, options.URL) + } + server.SetVerifyClientHTTPClient(httpClients) + server.SetVerifyClientURL(urls) + } + + if d.meshKey != "" { + server.SetMeshKey(d.meshKey) + } else if d.meshKeyPath != "" { + var meshKeyContent []byte + meshKeyContent, err = os.ReadFile(d.meshKeyPath) + if err != nil { + return err + } + err = checkMeshKey(string(meshKeyContent)) + if err != nil { + return E.Cause(err, "invalid mesh_psk_path file") + } + server.SetMeshKey(string(meshKeyContent)) + } + d.server = server + + derpMux := http.NewServeMux() + derpHandler := derphttp.Handler(server) + derpHandler = addWebSocketSupport(server, derpHandler) + derpMux.Handle("/derp", derpHandler) + + homeHandler, ok := getHomeHandler(d.home) + if !ok { + return E.New("invalid home value: ", d.home) + } + + derpMux.HandleFunc("/derp/probe", derphttp.ProbeHandler) + derpMux.HandleFunc("/derp/latency-check", derphttp.ProbeHandler) + derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx, d.domainResolver))) + derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + homeHandler.ServeHTTP(w, r) + })) + derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + io.WriteString(w, "User-agent: *\nDisallow: /\n") + })) + derpMux.Handle("/generate_204", http.HandlerFunc(derphttp.ServeNoContent)) + + err = d.tlsConfig.Start() + if err != nil { + return err + } + + tcpListener, err := d.listener.ListenTCP() + if err != nil { + return err + } + if len(d.tlsConfig.NextProtos()) == 0 { + d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) { + d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig) + httpServer := &http.Server{ + Handler: h2c.NewHandler(derpMux, &http2.Server{}), + } + go httpServer.Serve(tcpListener) + case adapter.StartStatePostStart: + if len(d.verifyClientEndpoint) > 0 { + var endpoints []*tailscale.LocalClient + endpointManager := service.FromContext[adapter.EndpointManager](d.ctx) + for _, endpointTag := range d.verifyClientEndpoint { + endpoint, loaded := endpointManager.Get(endpointTag) + if !loaded { + return E.New("verify_client_endpoint: endpoint not found: ", endpointTag) + } + tsEndpoint, isTailscale := endpoint.(*boxScale.Endpoint) + if !isTailscale { + return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag) + } + localClient, err := tsEndpoint.Server().LocalClient() + if err != nil { + return err + } + endpoints = append(endpoints, localClient) + } + d.server.SetVerifyClientLocalClient(endpoints) + } + if len(d.meshWith) > 0 { + if !d.server.HasMeshKey() { + return E.New("missing mesh psk") + } + for _, options := range d.meshWith { + err := d.startMeshWithHost(d.server, options) + if err != nil { + return err + } + } + } + } + return nil +} + +func checkMeshKey(meshKey string) error { + checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`) + if err != nil { + return err + } + if !checkRegex.MatchString(meshKey) { + return E.New("key must contain exactly 64 hex digits") + } + return nil +} + +func (d *Service) startMeshWithHost(derpServer *derp.Server, server option.DERPMeshOptions) error { + meshDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: d.ctx, + Options: server.DialerOptions, + RemoteIsDomain: server.ServerIsDomain(), + NewDialer: true, + }) + if err != nil { + return err + } + var hostname string + if server.Host != "" { + hostname = server.Host + } else { + hostname = server.Server + } + var stdConfig *tls.STDConfig + if server.TLS != nil && server.TLS.Enabled { + tlsConfig, err := tls.NewClient(d.ctx, hostname, common.PtrValueOrDefault(server.TLS)) + if err != nil { + return err + } + stdConfig, err = tlsConfig.Config() + if err != nil { + return err + } + } + logf := func(format string, args ...any) { + d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...))) + } + var meshHost string + if server.ServerPort == 0 || server.ServerPort == 443 { + meshHost = hostname + } else { + meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String() + } + meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), "https://"+meshHost+"/derp", logf, netmon.NewStatic()) + if err != nil { + return err + } + meshClient.TLSConfig = stdConfig + meshClient.MeshKey = derpServer.MeshKey() + meshClient.WatchConnectionChanges = true + meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) { + return meshDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }) + add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) } + remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) } + go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove) + return nil +} + +func (d *Service) Close() error { + return common.Close( + common.PtrOrNil(d.listener), + d.tlsConfig, + ) +} + +var homePage = ` +

DERP

+

+ This is a Tailscale DERP server. +

+ +

+ It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic + for Tailscale clients. +

+ +

+ Documentation: +

+ +