From fd9a076d252b6e868383bc23bc1a4fe44ce981f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 2 Sep 2026 22:28:27 +0800 Subject: [PATCH] Close idle connections of unreferenced outbounds and DNS servers --- adapter/outbound.go | 8 + adapter/outbound/manager.go | 6 +- box.go | 1 + common/httpclient/client.go | 22 ++- common/httpclient/managed_transport.go | 10 +- common/httpclient/manager.go | 26 +++ dns/transport_adapter.go | 15 ++ experimental/boxdd/cmd_run.go | 2 +- experimental/boxdd/server.go | 4 +- experimental/libbox/power_report.go | 4 +- go.mod | 6 +- go.sum | 12 +- protocol/cloudflare/inbound.go | 13 +- protocol/group/selector.go | 5 + protocol/group/urltest.go | 26 ++- protocol/hysteria/outbound.go | 5 + protocol/hysteria2/outbound.go | 5 + protocol/shadowsocks/outbound.go | 12 +- protocol/shadowtls/inbound.go | 19 +- protocol/snell/outbound.go | 10 +- protocol/ssh/outbound.go | 68 ++++++- protocol/tailscale/endpoint.go | 10 ++ protocol/trojan/inbound.go | 8 + protocol/trojan/outbound.go | 16 +- protocol/tuic/outbound.go | 9 +- protocol/vless/inbound.go | 24 ++- protocol/vless/outbound.go | 16 +- protocol/vmess/outbound.go | 16 +- route/reference.go | 235 +++++++++++++++++++++++++ route/reference_rule.go | 168 ++++++++++++++++++ service/ccm/service.go | 9 + service/derp/service.go | 6 + service/ocm/service.go | 9 + service/powerreport/promote.go | 9 +- service/powerreport/recorder.go | 2 +- service/usbip/client.go | 9 + transport/v2raygrpclite/client.go | 4 + transport/v2rayhttp/client.go | 4 + transport/v2rayquic/client.go | 93 ++++++++-- transport/v2rayquic/stream.go | 6 + 40 files changed, 851 insertions(+), 81 deletions(-) create mode 100644 route/reference.go create mode 100644 route/reference_rule.go diff --git a/adapter/outbound.go b/adapter/outbound.go index 3fafee51..f688f164 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -50,3 +50,11 @@ type OutboundManager interface { Remove(tag string) error Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error } + +type IdleConnectionCloser interface { + CloseIdleConnections() +} + +type Referrer interface { + References() []string +} diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index 52c5b6aa..5314549d 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -151,9 +151,9 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { if common.Contains(oTree, problemOutboundTag) { return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) } - m.access.Lock() - problemOutbound := m.outboundByTag[problemOutboundTag] - m.access.Unlock() + problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { + return it.Tag() == problemOutboundTag + }) if problemOutbound == nil { return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") } diff --git a/box.go b/box.go index 92cd1ecd..35037766 100644 --- a/box.go +++ b/box.go @@ -257,6 +257,7 @@ func New(options Options) (*Box, error) { service.MustRegisterPtr(ctx, clashMode) internalServices = append(internalServices, clashMode) } + internalServices = append(internalServices, route.NewReferenceManager(ctx, logFactory.NewLogger("reference"), options.Options)) ntpOptions := common.PtrValueOrDefault(options.NTP) var timeService *tls.TimeServiceWrapper if ntpOptions.Enabled { diff --git a/common/httpclient/client.go b/common/httpclient/client.go index b310152a..f5a56162 100644 --- a/common/httpclient/client.go +++ b/common/httpclient/client.go @@ -40,10 +40,12 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, return nil, err } return &ManagedTransport{ - dialer: rawDialer, - headers: headers, - host: host, - tag: tag, + dialer: rawDialer, + headers: headers, + host: host, + tag: tag, + detour: options.Detour, + defaultOutbound: options.DefaultOutbound, factory: func() (innerTransport, error) { return newAppleTransport(ctx, logger, rawDialer, options) }, @@ -69,11 +71,13 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string, return nil, err } managedTransport := &ManagedTransport{ - cheapRebuild: cheapRebuild, - dialer: rawDialer, - headers: headers, - host: host, - tag: tag, + cheapRebuild: cheapRebuild, + dialer: rawDialer, + headers: headers, + host: host, + tag: tag, + detour: options.Detour, + defaultOutbound: options.DefaultOutbound, factory: func() (innerTransport, error) { return newTransport(rawDialer, baseTLSConfig, options) }, diff --git a/common/httpclient/managed_transport.go b/common/httpclient/managed_transport.go index 779eccda..7e2f9472 100644 --- a/common/httpclient/managed_transport.go +++ b/common/httpclient/managed_transport.go @@ -25,10 +25,12 @@ type ManagedTransport struct { factory func() (innerTransport, error) cheapRebuild bool - dialer N.Dialer - headers http.Header - host string - tag string + dialer N.Dialer + headers http.Header + host string + tag string + detour string + defaultOutbound bool } type transportEpoch struct { diff --git a/common/httpclient/manager.go b/common/httpclient/manager.go index 614e4f83..45e05f21 100644 --- a/common/httpclient/manager.go +++ b/common/httpclient/manager.go @@ -9,11 +9,13 @@ import ( "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" ) var ( _ adapter.HTTPClientManager = (*Manager)(nil) _ adapter.LifecycleService = (*Manager)(nil) + _ adapter.Referrer = (*Manager)(nil) ) type Manager struct { @@ -177,3 +179,27 @@ func (m *Manager) Close() error { m.sharedTransports = nil return err } + +func (m *Manager) References() []string { + m.access.Lock() + defer m.access.Unlock() + var references []string + var needDefault bool + for _, transport := range m.managedTransports { + if transport.detour != "" { + references = append(references, transport.detour) + } else if transport.defaultOutbound { + needDefault = true + } + } + if needDefault { + outboundManager := service.FromContext[adapter.OutboundManager](m.ctx) + if outboundManager != nil { + defaultOutbound := outboundManager.Default() + if defaultOutbound != nil { + references = append(references, defaultOutbound.Tag()) + } + } + } + return references +} diff --git a/dns/transport_adapter.go b/dns/transport_adapter.go index 1e6620f2..7d2ebfa7 100644 --- a/dns/transport_adapter.go +++ b/dns/transport_adapter.go @@ -8,6 +8,7 @@ type TransportAdapter struct { transportType string transportTag string dependencies []string + references []string } func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter { @@ -23,10 +24,15 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" { dependencies = append(dependencies, localOptions.DomainResolver.Server) } + var references []string + if localOptions.Detour != "" { + references = []string{localOptions.Detour} + } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, + references: references, } } @@ -35,10 +41,15 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" { dependencies = append(dependencies, remoteOptions.DomainResolver.Server) } + var references []string + if remoteOptions.Detour != "" { + references = []string{remoteOptions.Detour} + } return TransportAdapter{ transportType: transportType, transportTag: transportTag, dependencies: dependencies, + references: references, } } @@ -53,3 +64,7 @@ func (a *TransportAdapter) Tag() string { func (a *TransportAdapter) Dependencies() []string { return a.dependencies } + +func (a *TransportAdapter) References() []string { + return a.references +} diff --git a/experimental/boxdd/cmd_run.go b/experimental/boxdd/cmd_run.go index be5452b8..f5518bad 100644 --- a/experimental/boxdd/cmd_run.go +++ b/experimental/boxdd/cmd_run.go @@ -67,7 +67,7 @@ func prepareWorkingDirectory() error { return err } libbox.PromoteOOMDraft() - libbox.PromotePowerReportDraft() + libbox.DiscardPowerReportDraft() return nil } diff --git a/experimental/boxdd/server.go b/experimental/boxdd/server.go index 8e9f6b16..ad9df8a2 100644 --- a/experimental/boxdd/server.go +++ b/experimental/boxdd/server.go @@ -203,7 +203,7 @@ func (d *Daemon) configureWorkingDirectoryLocked(directory string) error { return err } libbox.PromoteOOMDraft() - libbox.PromotePowerReportDraft() + libbox.DiscardPowerReportDraft() d.runtimeWorkingDirectory = directory return nil } @@ -262,7 +262,7 @@ func (d *Daemon) stopServiceLocked(ownerUserID string) error { } } d.powerManager.Close() - libbox.PromotePowerReportDraft() + libbox.DiscardPowerReportDraft() directory := userWorkingDirectory(ownerUserID) crashReportError := tagUnownedReports(filepath.Join(directory, crashReportsDirectoryName), ownerUserID) if crashReportError != nil { diff --git a/experimental/libbox/power_report.go b/experimental/libbox/power_report.go index 29648cf6..a31afd2d 100644 --- a/experimental/libbox/power_report.go +++ b/experimental/libbox/power_report.go @@ -36,6 +36,6 @@ func PowerReportOptions(startedService *daemon.StartedService) powerreport.Optio } } -func PromotePowerReportDraft() { - powerreport.PromoteDraft(sWorkingPath) +func DiscardPowerReportDraft() { + powerreport.DiscardDraft(sWorkingPath) } diff --git a/go.mod b/go.mod index 69ab9c02..05bd89dc 100644 --- a/go.mod +++ b/go.mod @@ -47,14 +47,14 @@ require ( github.com/sagernet/quic-go v0.61.0-sing-box-mod.7 github.com/sagernet/sing v0.9.6-0.20260927091426-5f9aad7def20 github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 - github.com/sagernet/sing-mux v0.3.8 + github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6 - github.com/sagernet/sing-quic v0.7.1-0.20260924092235-6a3a24d65b99 + github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1 - github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3 + github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f github.com/sagernet/sing-tun v0.9.6-0.20260924001923-ddaa4ca25e3b github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca github.com/sagernet/sing-vmess v0.2.8 diff --git a/go.sum b/go.sum index 76c18a0b..0dfadd2d 100644 --- a/go.sum +++ b/go.sum @@ -322,22 +322,22 @@ github.com/sagernet/sing v0.9.6-0.20260927091426-5f9aad7def20 h1:FRiFCD9vYplVTPr github.com/sagernet/sing v0.9.6-0.20260927091426-5f9aad7def20/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8= github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg= -github.com/sagernet/sing-mux v0.3.8 h1:mM0TLUkr36aEhgUHi9Qh60qcAbD2fZcmnJ30937xxrY= -github.com/sagernet/sing-mux v0.3.8/go.mod h1:jricpwCOndDsTEM1TM3piJvhL/2Mm7OhLkm+JadJJ1Q= +github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a h1:VrlU4QE8Bh4kEeLgvvWLBtyTiuXqc6/5vf3B+xQCTRE= +github.com/sagernet/sing-mux v0.3.9-0.20260927144857-0257b84c582a/go.mod h1:jricpwCOndDsTEM1TM3piJvhL/2Mm7OhLkm+JadJJ1Q= github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df h1:ogyC7KPTWueN7kXFhKSdHeS5/t/zBjQgAOpWjqnrMos= github.com/sagernet/sing-openconnect v0.0.0-20260810065514-53aa8058f8df/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg= github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6 h1:ZAvhkor0prJ8KEX9EmTEH+gJ1WlA3ffjS8GZT6KKq9c= github.com/sagernet/sing-openvpn v0.0.0-20260729104525-103eb5fe5eb6/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw= -github.com/sagernet/sing-quic v0.7.1-0.20260924092235-6a3a24d65b99 h1:zxN7hz90xUa03Nbrnp6nutArlrQaVHOOvbTYVGx4/PI= -github.com/sagernet/sing-quic v0.7.1-0.20260924092235-6a3a24d65b99/go.mod h1:rJFeVxbEej2iuyeCJ3drKW+p5rOgtLIW3XhZ6ty7Fig= +github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b h1:2Jx/URJAPt32MEbLmAR5nzH87tO6NQZLRfRFvFqBWuk= +github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b/go.mod h1:rJFeVxbEej2iuyeCJ3drKW+p5rOgtLIW3XhZ6ty7Fig= 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 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkGCKNXhbaM= github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3 h1:wjEw0lmCSC+oqh7GD4rbMB/dk7+hZ2VopUj4xM5/Jrc= -github.com/sagernet/sing-snell v0.0.0-20260829071736-20f2eaec77c3/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM= +github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f h1:tbTCI+iis5sTmua7a6Q7vebcSRURok/LGgnEafnQAsc= +github.com/sagernet/sing-snell v0.0.0-20260904135315-bc5a12ac736f/go.mod h1:et8Lws4f5QbOrY65DmjevHGup3mijJkhswkto6cwciM= github.com/sagernet/sing-tun v0.9.6-0.20260924001923-ddaa4ca25e3b h1:tjQUA4fQi0OWDMMYfRigzvNlBTYyP/01e+P9BT/RZ4g= github.com/sagernet/sing-tun v0.9.6-0.20260924001923-ddaa4ca25e3b/go.mod h1:3EgPst7agntRO7D6GOsiZ1l9FoqdLeuWmKT5TnWkmf0= github.com/sagernet/sing-usbip v0.0.0-20260817040617-28bd42667eca h1:5wA+IE0Fq1CGVLOgSpm0gKKZ03HzugcJ2JyhmDqeX6A= diff --git a/protocol/cloudflare/inbound.go b/protocol/cloudflare/inbound.go index e190d37e..d69dac0b 100644 --- a/protocol/cloudflare/inbound.go +++ b/protocol/cloudflare/inbound.go @@ -16,6 +16,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-cloudflared" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -79,14 +80,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } return &Inbound{ - Adapter: inbound.NewAdapter(C.TypeCloudflared, tag), - service: service, + Adapter: inbound.NewAdapter(C.TypeCloudflared, tag), + service: service, + references: common.FilterNotDefault([]string{options.ControlDialer.Detour, options.TunnelDialer.Detour}), }, nil } type Inbound struct { inbound.Adapter - service *cloudflared.Service + service *cloudflared.Service + references []string } func (i *Inbound) Start(stage adapter.StartStage) error { @@ -255,6 +258,10 @@ func (h *icmpRouterHandler) RouteICMPFlow(source netip.Addr, destination netip.A } } +func (i *Inbound) References() []string { + return i.references +} + var ( _ N.HandshakeSuccess = (*routedConn)(nil) _ N.ReaderWithUpstream = (*routedConn)(nil) diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 3d1a8bf4..d23ffeb2 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -25,6 +25,7 @@ func RegisterSelector(registry *outbound.Registry) { var ( _ adapter.OutboundGroup = (*Selector)(nil) + _ adapter.Referrer = (*Selector)(nil) _ adapter.ConnectionHandler = (*Selector)(nil) _ adapter.PacketConnectionHandler = (*Selector)(nil) ) @@ -120,6 +121,10 @@ func (s *Selector) All() []string { return s.tags } +func (s *Selector) References() []string { + return []string{s.Now()} +} + func (s *Selector) SelectOutbound(tag string) bool { detour, loaded := s.outbounds[tag] if !loaded { diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index 1bb6b113..a264b2dd 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -32,6 +32,7 @@ func RegisterURLTest(registry *outbound.Registry) { var ( _ adapter.OutboundGroup = (*URLTest)(nil) _ adapter.InterfaceUpdateListener = (*URLTest)(nil) + _ adapter.Referrer = (*URLTest)(nil) ) type URLTest struct { @@ -111,6 +112,21 @@ func (s *URLTest) All() []string { return s.tags } +func (s *URLTest) References() []string { + group := s.group + if group == nil { + return nil + } + var references []string + if group.selectedOutboundTCP != nil { + references = append(references, group.selectedOutboundTCP.Tag()) + } + if group.selectedOutboundUDP != nil && group.selectedOutboundUDP != group.selectedOutboundTCP { + references = append(references, group.selectedOutboundUDP.Tag()) + } + return references +} + func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) { return s.group.URLTest(ctx) } @@ -488,20 +504,28 @@ func (b *urlTestBatch) test(outbounds []adapter.Outbound, link string, interval func (g *URLTestGroup) performUpdateCheck() { g.updateAccess.Lock() defer g.updateAccess.Unlock() - var updated bool + var ( + updated bool + selected bool + ) if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) { if g.selectedOutboundTCP != nil { updated = true } g.selectedOutboundTCP = outbound + selected = true } if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) { if g.selectedOutboundUDP != nil { updated = true } g.selectedOutboundUDP = outbound + selected = true } if updated { g.interruptGroup.Interrupt(g.interruptExternalConnections) } + if selected { + g.history.NotifyUpdated() + } } diff --git a/protocol/hysteria/outbound.go b/protocol/hysteria/outbound.go index 937d55db..61760d98 100644 --- a/protocol/hysteria/outbound.go +++ b/protocol/hysteria/outbound.go @@ -30,6 +30,7 @@ func RegisterOutbound(registry *outbound.Registry) { var ( _ adapter.Outbound = (*tuic.Outbound)(nil) _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) ) type Outbound struct { @@ -118,6 +119,10 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { h.client.CloseWithError(E.New("network changed")) } +func (h *Outbound) CloseIdleConnections() { + h.client.CloseIdleConnections() +} + func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 392b0e40..fd142e5d 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -37,6 +37,7 @@ func RegisterOutbound(registry *outbound.Registry) { var ( _ adapter.Outbound = (*tuic.Outbound)(nil) _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) ) type Outbound struct { @@ -210,6 +211,10 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { h.client.CloseWithError(E.New("network changed")) } +func (h *Outbound) CloseIdleConnections() { + h.client.CloseIdleConnections() +} + func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/protocol/shadowsocks/outbound.go b/protocol/shadowsocks/outbound.go index 10dd204d..33532538 100644 --- a/protocol/shadowsocks/outbound.go +++ b/protocol/shadowsocks/outbound.go @@ -26,7 +26,11 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound) } -var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) +var ( + _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -136,6 +140,12 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { } } +func (h *Outbound) CloseIdleConnections() { + if h.multiplexDialer != nil { + h.multiplexDialer.CloseIdleConnections() + } +} + func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer)) } diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go index abc3b8bd..5781a11c 100644 --- a/protocol/shadowtls/inbound.go +++ b/protocol/shadowtls/inbound.go @@ -26,10 +26,11 @@ func RegisterInbound(registry *inbound.Registry) { type Inbound struct { inbound.Adapter - router adapter.Router - logger logger.ContextLogger - listener *listener.Listener - service *shadowtls.Service + router adapter.Router + logger logger.ContextLogger + listener *listener.Listener + service *shadowtls.Service + references []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (adapter.Inbound, error) { @@ -48,6 +49,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) if options.HandshakeForServerName != nil { for _, entry := range options.HandshakeForServerName.Entries() { + if entry.Value.Detour != "" { + inbound.references = append(inbound.references, entry.Value.Detour) + } handshakeDialer, err := dialer.New(ctx, entry.Value.DialerOptions, entry.Value.ServerIsDomain()) if err != nil { return nil, err @@ -67,6 +71,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } + if options.Handshake.Detour != "" { + inbound.references = append(inbound.references, options.Handshake.Detour) + } service, err := shadowtls.NewService(shadowtls.ServiceConfig{ Version: options.Version, Password: options.Password, @@ -139,3 +146,7 @@ func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, sou } h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } + +func (h *Inbound) References() []string { + return h.references +} diff --git a/protocol/snell/outbound.go b/protocol/snell/outbound.go index 01160a5e..90ca6469 100644 --- a/protocol/snell/outbound.go +++ b/protocol/snell/outbound.go @@ -32,12 +32,16 @@ type Outbound struct { serverAddr M.Socksaddr } -var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) +var ( + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type snellClient interface { snellprotocol.Method DialContext(ctx context.Context, destination M.Socksaddr) (net.Conn, error) Reset() + CloseIdleConnections() Close() error } @@ -143,6 +147,10 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { h.client.Reset() } +func (h *Outbound) CloseIdleConnections() { + h.client.CloseIdleConnections() +} + func (h *Outbound) Close() error { return h.client.Close() } diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index ec2ab27d..0033e92b 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -32,7 +32,10 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.SSHOutboundOptions](registry, C.TypeSSH, NewOutbound) } -var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) +var ( + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -50,6 +53,8 @@ type Outbound struct { clientAccess sync.Mutex clientConn net.Conn client *ssh.Client + streams int + draining bool } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) { @@ -127,14 +132,12 @@ func randomVersion() string { } func (s *Outbound) connect(ctx context.Context) (client *ssh.Client, err error) { - if s.client != nil { - return s.client, nil - } - s.clientAccess.Lock() defer s.clientAccess.Unlock() if s.client != nil { + s.streams++ + s.draining = false return s.client, nil } @@ -193,13 +196,17 @@ func (s *Outbound) connect(ctx context.Context) (client *ssh.Client, err error) s.clientConn = conn s.client = client + s.streams = 1 + s.draining = false go func() { client.Wait() conn.Close() s.clientAccess.Lock() - s.client = nil - s.clientConn = nil + if s.client == client { + s.client = nil + s.clientConn = nil + } s.clientAccess.Unlock() }() @@ -210,6 +217,42 @@ func (s *Outbound) InterfaceUpdated(ctx context.Context) { common.Close(s.clientConn) } +func (s *Outbound) CloseIdleConnections() { + s.clientAccess.Lock() + if s.client == nil { + s.clientAccess.Unlock() + return + } + s.draining = true + if s.streams > 0 { + s.clientAccess.Unlock() + return + } + clientConn := s.clientConn + s.client = nil + s.clientConn = nil + s.clientAccess.Unlock() + common.Close(clientConn) +} + +func (s *Outbound) releaseStream(client *ssh.Client) { + s.clientAccess.Lock() + if s.client != client { + s.clientAccess.Unlock() + return + } + s.streams-- + if !s.draining || s.streams > 0 { + s.clientAccess.Unlock() + return + } + clientConn := s.clientConn + s.client = nil + s.clientConn = nil + s.clientAccess.Unlock() + common.Close(clientConn) +} + func (s *Outbound) Close() error { return common.Close(s.clientConn) } @@ -221,9 +264,10 @@ func (s *Outbound) DialContext(ctx context.Context, network string, destination } conn, err := client.Dial(network, destination.String()) if err != nil { + s.releaseStream(client) return nil, err } - return &chanConnWrapper{Conn: conn}, nil + return &chanConnWrapper{Conn: conn, onClose: func() { s.releaseStream(client) }}, nil } func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { @@ -232,6 +276,14 @@ func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n type chanConnWrapper struct { net.Conn + closeOnce sync.Once + onClose func() +} + +func (c *chanConnWrapper) Close() error { + err := c.Conn.Close() + c.closeOnce.Do(c.onClose) + return err } func (c *chanConnWrapper) SetDeadline(t time.Time) error { diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index f46a4869..277a2394 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -68,6 +68,7 @@ import ( var ( _ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil) _ adapter.InterfaceUpdateListener = (*Endpoint)(nil) + _ adapter.Referrer = (*Endpoint)(nil) _ dialer.PacketDialerWithDestination = (*Endpoint)(nil) _ tun.Port = (*Endpoint)(nil) ) @@ -89,6 +90,7 @@ type Endpoint struct { dnsRouter adapter.DNSRouter network adapter.NetworkManager platformInterface adapter.PlatformInterface + detour string server *tsnet.Server stack *stack.Stack icmpForwarder *tun.ICMPForwarder @@ -200,6 +202,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL queryOptions: dialerQueryOptions, network: service.FromContext[adapter.NetworkManager](ctx), platformInterface: platformInterface, + detour: options.Detour, server: &tsnet.Server{ Dir: stateDirectory, Hostname: hostname, @@ -251,6 +254,13 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL }, nil } +func (t *Endpoint) References() []string { + if t.detour == "" { + return nil + } + return []string{t.detour} +} + func (t *Endpoint) Start(stage adapter.StartStage) error { switch stage { case adapter.StartStateInitialize: diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index 920589b4..87a84754 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -40,6 +40,7 @@ type Inbound struct { fallbackAddr M.Socksaddr fallbackAddrTLSNextProto map[string]M.Socksaddr transport adapter.V2RayServerTransport + references []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (adapter.Inbound, error) { @@ -61,6 +62,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return nil, err } inbound.tlsConfig = tlsConfig + if options.TLS.Reality != nil && options.TLS.Reality.Enabled && options.TLS.Reality.Handshake.Detour != "" { + inbound.references = []string{options.TLS.Reality.Handshake.Detour} + } } var fallbackHandler N.TCPConnectionHandlerEx if options.Fallback != nil && options.Fallback.Server != "" || len(options.FallbackForALPN) > 0 { @@ -260,3 +264,7 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } + +func (h *Inbound) References() []string { + return h.references +} diff --git a/protocol/trojan/outbound.go b/protocol/trojan/outbound.go index 9abe76d4..591d44a9 100644 --- a/protocol/trojan/outbound.go +++ b/protocol/trojan/outbound.go @@ -26,7 +26,11 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound) } -var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) +var ( + _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -122,6 +126,16 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { } } +func (h *Outbound) CloseIdleConnections() { + transportCloser, isTransportCloser := h.transport.(adapter.IdleConnectionCloser) + if isTransportCloser { + transportCloser.CloseIdleConnections() + } + if h.multiplexDialer != nil { + h.multiplexDialer.CloseIdleConnections() + } +} + func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } diff --git a/protocol/tuic/outbound.go b/protocol/tuic/outbound.go index 53f65ccb..615d41fe 100644 --- a/protocol/tuic/outbound.go +++ b/protocol/tuic/outbound.go @@ -30,7 +30,10 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, NewOutbound) } -var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) +var ( + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -145,6 +148,10 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { _ = h.client.CloseWithError(E.New("network changed")) } +func (h *Outbound) CloseIdleConnections() { + h.client.CloseIdleConnections() +} + func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go index 1589bfb0..4f77fb13 100644 --- a/protocol/vless/inbound.go +++ b/protocol/vless/inbound.go @@ -35,14 +35,15 @@ var _ adapter.TCPInjectableInbound = (*Inbound)(nil) type Inbound struct { inbound.Adapter - ctx context.Context - router adapter.ConnectionRouterEx - logger logger.ContextLogger - listener *listener.Listener - users []option.VLESSUser - service *vless.Service[int] - tlsConfig tls.ServerConfig - transport adapter.V2RayServerTransport + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + users []option.VLESSUser + service *vless.Service[int] + tlsConfig tls.ServerConfig + transport adapter.V2RayServerTransport + references []string } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) { @@ -81,6 +82,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } + if options.TLS.Reality != nil && options.TLS.Reality.Enabled && options.TLS.Reality.Handshake.Detour != "" { + inbound.references = []string{options.TLS.Reality.Handshake.Detour} + } } if options.Transport != nil { inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) @@ -220,3 +224,7 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnection(ctx, conn, metadata, onClose) } + +func (h *Inbound) References() []string { + return h.references +} diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go index 1356a589..8be11db2 100644 --- a/protocol/vless/outbound.go +++ b/protocol/vless/outbound.go @@ -27,7 +27,11 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound) } -var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) +var ( + _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -142,6 +146,16 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { } } +func (h *Outbound) CloseIdleConnections() { + transportCloser, isTransportCloser := h.transport.(adapter.IdleConnectionCloser) + if isTransportCloser { + transportCloser.CloseIdleConnections() + } + if h.multiplexDialer != nil { + h.multiplexDialer.CloseIdleConnections() + } +} + func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go index 7b16fc90..4ac274b5 100644 --- a/protocol/vmess/outbound.go +++ b/protocol/vmess/outbound.go @@ -27,7 +27,11 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound) } -var _ adapter.OutboundWithMultiplex = (*Outbound)(nil) +var ( + _ adapter.OutboundWithMultiplex = (*Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + _ adapter.IdleConnectionCloser = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter @@ -120,6 +124,16 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) { } } +func (h *Outbound) CloseIdleConnections() { + transportCloser, isTransportCloser := h.transport.(adapter.IdleConnectionCloser) + if isTransportCloser { + transportCloser.CloseIdleConnections() + } + if h.multiplexDialer != nil { + h.multiplexDialer.CloseIdleConnections() + } +} + func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } diff --git a/route/reference.go b/route/reference.go new file mode 100644 index 00000000..e2f5ba41 --- /dev/null +++ b/route/reference.go @@ -0,0 +1,235 @@ +package route + +import ( + "context" + "slices" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/urltest" + "github.com/sagernet/sing-box/experimental/clashmode" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/service" +) + +var _ adapter.LifecycleService = (*ReferenceManager)(nil) + +type ReferenceManager struct { + ctx context.Context + logger log.ContextLogger + rules []option.Rule + dnsRules []option.DNSRule + staticOutbounds []string + staticTransports []string + subscriber *observable.Subscriber[struct{}] + unreferencedOutbounds map[string]bool + unreferencedTransports map[string]bool +} + +func NewReferenceManager(ctx context.Context, logger log.ContextLogger, options option.Options) *ReferenceManager { + var ( + staticOutbounds []string + staticTransports []string + ) + if options.NTP != nil && options.NTP.Enabled && options.NTP.Detour != "" { + staticOutbounds = append(staticOutbounds, options.NTP.Detour) + } + for _, outboundOptions := range options.Outbounds { + staticTransports = appendDomainResolver(staticTransports, outboundOptions.Options) + } + for _, endpointOptions := range options.Endpoints { + staticTransports = appendDomainResolver(staticTransports, endpointOptions.Options) + } + var ( + rules []option.Rule + dnsRules []option.DNSRule + ) + if options.Route != nil { + rules = options.Route.Rules + } + if options.DNS != nil { + dnsRules = options.DNS.Rules + } + return &ReferenceManager{ + ctx: ctx, + logger: logger, + rules: rules, + dnsRules: dnsRules, + staticOutbounds: staticOutbounds, + staticTransports: staticTransports, + } +} + +func appendDomainResolver(transports []string, rawOptions any) []string { + dialerOptionsWrapper, isDialerOptionsWrapper := rawOptions.(option.DialerOptionsWrapper) + if !isDialerOptionsWrapper { + return transports + } + dialerOptions := dialerOptionsWrapper.TakeDialerOptions() + if dialerOptions.DomainResolver == nil || dialerOptions.DomainResolver.Server == "" { + return transports + } + return append(transports, dialerOptions.DomainResolver.Server) +} + +func (m *ReferenceManager) Name() string { + return "reference manager" +} + +func (m *ReferenceManager) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStarted { + return nil + } + m.subscriber = observable.NewSubscriber[struct{}](1) + history := service.PtrFromContext[urltest.HistoryStorage](m.ctx) + if history != nil { + history.AddUpdateHook(m.subscriber) + } + clashMode := service.PtrFromContext[clashmode.Manager](m.ctx) + if clashMode != nil { + clashMode.AddUpdateHook(m.subscriber) + } + m.update() + go m.loop() + return nil +} + +func (m *ReferenceManager) Close() error { + if m.subscriber != nil { + m.subscriber.Close() + } + return nil +} + +func (m *ReferenceManager) loop() { + subscription, done := m.subscriber.Subscription() + for { + select { + case <-subscription: + case <-done: + return + } + m.update() + } +} + +func (m *ReferenceManager) update() { + var mode string + clashMode := service.PtrFromContext[clashmode.Manager](m.ctx) + if clashMode != nil { + mode = clashMode.Mode() + } + outboundManager := service.FromContext[adapter.OutboundManager](m.ctx) + endpointManager := service.FromContext[adapter.EndpointManager](m.ctx) + inboundManager := service.FromContext[adapter.InboundManager](m.ctx) + serviceManager := service.FromContext[adapter.ServiceManager](m.ctx) + transportManager := service.FromContext[adapter.DNSTransportManager](m.ctx) + networkManager := service.FromContext[adapter.NetworkManager](m.ctx) + httpClientManager := service.FromContext[adapter.HTTPClientManager](m.ctx) + + transportQueue := slices.Clone(m.staticTransports) + outboundQueue := slices.Clone(m.staticOutbounds) + if !collectDNSRuleReferences(m.dnsRules, mode, &transportQueue) { + defaultTransport := transportManager.Default() + if defaultTransport != nil { + transportQueue = append(transportQueue, defaultTransport.Tag()) + } + } + transportQueue = append(transportQueue, networkManager.DefaultOptions().DomainResolver) + if !collectRuleReferences(m.rules, mode, &outboundQueue, &transportQueue) { + defaultOutbound := outboundManager.Default() + if defaultOutbound != nil { + outboundQueue = append(outboundQueue, defaultOutbound.Tag()) + } + } + for _, endpoint := range endpointManager.Endpoints() { + outboundQueue = append(outboundQueue, endpoint.Tag()) + } + for _, inbound := range inboundManager.Inbounds() { + referrer, isReferrer := inbound.(adapter.Referrer) + if isReferrer { + outboundQueue = append(outboundQueue, referrer.References()...) + } + } + for _, boxService := range serviceManager.Services() { + referrer, isReferrer := boxService.(adapter.Referrer) + if isReferrer { + outboundQueue = append(outboundQueue, referrer.References()...) + } + } + referrer, isReferrer := httpClientManager.(adapter.Referrer) + if isReferrer { + outboundQueue = append(outboundQueue, referrer.References()...) + } + + referencedTransports := make(map[string]bool) + for len(transportQueue) > 0 { + tag := transportQueue[0] + transportQueue = transportQueue[1:] + if tag == "" || referencedTransports[tag] { + continue + } + transport, loaded := transportManager.Transport(tag) + if !loaded { + continue + } + referencedTransports[tag] = true + transportQueue = append(transportQueue, transport.Dependencies()...) + transportReferrer, isTransportReferrer := transport.(adapter.Referrer) + if isTransportReferrer { + outboundQueue = append(outboundQueue, transportReferrer.References()...) + } + } + referencedOutbounds := make(map[string]bool) + for len(outboundQueue) > 0 { + tag := outboundQueue[0] + outboundQueue = outboundQueue[1:] + if tag == "" || referencedOutbounds[tag] { + continue + } + outbound, loaded := outboundManager.Outbound(tag) + if !loaded { + continue + } + referencedOutbounds[tag] = true + outboundReferrer, isOutboundReferrer := outbound.(adapter.Referrer) + if isOutboundReferrer { + outboundQueue = append(outboundQueue, outboundReferrer.References()...) + } else { + outboundQueue = append(outboundQueue, outbound.Dependencies()...) + } + } + + unreferencedOutbounds := make(map[string]bool) + for _, outbound := range outboundManager.Outbounds() { + tag := outbound.Tag() + if referencedOutbounds[tag] { + continue + } + unreferencedOutbounds[tag] = true + closer, isCloser := outbound.(adapter.IdleConnectionCloser) + if !isCloser { + continue + } + if !m.unreferencedOutbounds[tag] { + m.logger.Debug("outbound/", outbound.Type(), "[", tag, "] is unreferenced, closing idle connections") + } + closer.CloseIdleConnections() + } + m.unreferencedOutbounds = unreferencedOutbounds + + unreferencedTransports := make(map[string]bool) + for _, transport := range transportManager.Transports() { + tag := transport.Tag() + if referencedTransports[tag] { + continue + } + unreferencedTransports[tag] = true + if m.unreferencedTransports != nil && !m.unreferencedTransports[tag] { + m.logger.Debug("dns/", transport.Type(), "[", tag, "] is unreferenced, resetting") + transport.Reset() + } + } + m.unreferencedTransports = unreferencedTransports +} diff --git a/route/reference_rule.go b/route/reference_rule.go new file mode 100644 index 00000000..86464b65 --- /dev/null +++ b/route/reference_rule.go @@ -0,0 +1,168 @@ +package route + +import ( + "reflect" + "strings" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +type staticMatch uint8 + +const ( + staticMatchUnknown staticMatch = iota + staticMatchNever + staticMatchAlways +) + +func (m staticMatch) invert() staticMatch { + switch m { + case staticMatchNever: + return staticMatchAlways + case staticMatchAlways: + return staticMatchNever + default: + return staticMatchUnknown + } +} + +func evaluateClashMode(clashMode string, mode string, invert bool, otherConditions bool) staticMatch { + match := staticMatchUnknown + if clashMode != "" && !strings.EqualFold(clashMode, mode) { + match = staticMatchNever + } else if !otherConditions { + match = staticMatchAlways + } + if invert { + match = match.invert() + } + return match +} + +func evaluateLogical(matches []staticMatch, logicalMode string, invert bool) staticMatch { + var match staticMatch + if logicalMode == C.LogicalTypeAnd { + match = staticMatchAlways + } else { + match = staticMatchNever + } + for _, ruleMatch := range matches { + if logicalMode == C.LogicalTypeAnd && ruleMatch == staticMatchNever { + match = staticMatchNever + break + } + if logicalMode == C.LogicalTypeOr && ruleMatch == staticMatchAlways { + match = staticMatchAlways + break + } + if ruleMatch == staticMatchUnknown { + match = staticMatchUnknown + } + } + if invert { + match = match.invert() + } + return match +} + +func evaluateRule(rule option.Rule, mode string) staticMatch { + switch rule.Type { + case C.RuleTypeDefault: + conditions := rule.DefaultOptions.RawDefaultRule + conditions.ClashMode = "" + conditions.Invert = false + return evaluateClashMode(rule.DefaultOptions.ClashMode, mode, rule.DefaultOptions.Invert, !reflect.DeepEqual(conditions, option.RawDefaultRule{})) + case C.RuleTypeLogical: + matches := make([]staticMatch, 0, len(rule.LogicalOptions.Rules)) + for _, subRule := range rule.LogicalOptions.Rules { + matches = append(matches, evaluateRule(subRule, mode)) + } + return evaluateLogical(matches, rule.LogicalOptions.Mode, rule.LogicalOptions.Invert) + default: + return staticMatchUnknown + } +} + +func evaluateDNSRule(rule option.DNSRule, mode string) staticMatch { + switch rule.Type { + case C.RuleTypeDefault: + conditions := rule.DefaultOptions.RawDefaultDNSRule + conditions.ClashMode = "" + conditions.Invert = false + return evaluateClashMode(rule.DefaultOptions.ClashMode, mode, rule.DefaultOptions.Invert, !reflect.DeepEqual(conditions, option.RawDefaultDNSRule{})) + case C.RuleTypeLogical: + matches := make([]staticMatch, 0, len(rule.LogicalOptions.Rules)) + for _, subRule := range rule.LogicalOptions.Rules { + matches = append(matches, evaluateDNSRule(subRule, mode)) + } + return evaluateLogical(matches, rule.LogicalOptions.Mode, rule.LogicalOptions.Invert) + default: + return staticMatchUnknown + } +} + +func collectRuleReferences(rules []option.Rule, mode string, outbounds *[]string, transports *[]string) (shadowed bool) { + for _, rule := range rules { + match := evaluateRule(rule, mode) + if match == staticMatchNever { + continue + } + var action option.RuleAction + switch rule.Type { + case C.RuleTypeDefault: + action = rule.DefaultOptions.RuleAction + case C.RuleTypeLogical: + action = rule.LogicalOptions.RuleAction + } + var final bool + switch action.Action { + case C.RuleActionTypeRoute: + *outbounds = append(*outbounds, action.RouteOptions.Outbound) + final = true + case C.RuleActionTypeBypass: + if action.BypassOptions.Outbound != "" { + *outbounds = append(*outbounds, action.BypassOptions.Outbound) + final = true + } + case C.RuleActionTypeDirect, C.RuleActionTypeReject, C.RuleActionTypeHijackDNS: + final = true + case C.RuleActionTypeResolve: + *transports = append(*transports, action.ResolveOptions.Server) + } + if final && match == staticMatchAlways { + return true + } + } + return false +} + +func collectDNSRuleReferences(rules []option.DNSRule, mode string, transports *[]string) (shadowed bool) { + for _, rule := range rules { + match := evaluateDNSRule(rule, mode) + if match == staticMatchNever { + continue + } + var action option.DNSRuleAction + switch rule.Type { + case C.RuleTypeDefault: + action = rule.DefaultOptions.DNSRuleAction + case C.RuleTypeLogical: + action = rule.LogicalOptions.DNSRuleAction + } + var final bool + switch action.Action { + case C.RuleActionTypeRoute: + *transports = append(*transports, action.RouteOptions.Server) + final = true + case C.RuleActionTypeEvaluate: + *transports = append(*transports, action.EvaluateOptions.Server) + case C.RuleActionTypeRespond, C.RuleActionTypeReject, C.RuleActionTypePredefined: + final = true + } + if final && match == staticMatchAlways { + return true + } + } + return false +} diff --git a/service/ccm/service.go b/service/ccm/service.go index f143c693..a211a3ef 100644 --- a/service/ccm/service.go +++ b/service/ccm/service.go @@ -114,6 +114,7 @@ type Service struct { ctx context.Context logger log.ContextLogger credentialPath string + detour string credentials *oauthCredentials users []option.CCMUser httpClient *http.Client @@ -168,6 +169,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio service := &Service{ Adapter: boxService.NewAdapter(C.TypeCCM, tag), + detour: options.Detour, ctx: ctx, logger: logger, credentialPath: options.CredentialPath, @@ -594,3 +596,10 @@ func (s *Service) Close() error { return err } + +func (s *Service) References() []string { + if s.detour == "" { + return nil + } + return []string{s.detour} +} diff --git a/service/derp/service.go b/service/derp/service.go index e251a799..485bc41e 100644 --- a/service/derp/service.go +++ b/service/derp/service.go @@ -512,3 +512,9 @@ func (d *Service) loopSTUNPacket(packetConn *net.UDPConn) { packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort) } } + +func (d *Service) References() []string { + return common.FilterNotDefault(common.Map(d.meshWith, func(it *option.DERPMeshOptions) string { + return it.Detour + })) +} diff --git a/service/ocm/service.go b/service/ocm/service.go index d02e923e..5b213e23 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -128,6 +128,7 @@ type Service struct { ctx context.Context logger log.ContextLogger credentialPath string + detour string credentials *oauthCredentials users []option.OCMUser dialer N.Dialer @@ -187,6 +188,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio service := &Service{ Adapter: boxService.NewAdapter(C.TypeOCM, tag), + detour: options.Detour, ctx: ctx, logger: logger, credentialPath: options.CredentialPath, @@ -706,3 +708,10 @@ func (s *Service) startWebSocketShutdown() []*webSocketSession { } return webSocketSessions } + +func (s *Service) References() []string { + if s.detour == "" { + return nil + } + return []string{s.detour} +} diff --git a/service/powerreport/promote.go b/service/powerreport/promote.go index f1b0f8bb..e410396a 100644 --- a/service/powerreport/promote.go +++ b/service/powerreport/promote.go @@ -6,15 +6,12 @@ import ( "strconv" ) -func PromoteDraft(basePath string) { - promoteDirectory(filepath.Join(basePath, DraftDirectoryName), filepath.Join(basePath, ReportsDirectoryName)) +func DiscardDraft(basePath string) { + os.RemoveAll(filepath.Join(basePath, DraftDirectoryName)) } func finalizeDraft(draftPath string) { - promoteDirectory(draftPath, filepath.Join(filepath.Dir(draftPath), ReportsDirectoryName)) -} - -func promoteDirectory(draftPath string, reportsPath string) { + reportsPath := filepath.Join(filepath.Dir(draftPath), ReportsDirectoryName) info, err := os.Stat(draftPath) if err != nil || !info.IsDir() { return diff --git a/service/powerreport/recorder.go b/service/powerreport/recorder.go index 0b903a8d..5cb2767a 100644 --- a/service/powerreport/recorder.go +++ b/service/powerreport/recorder.go @@ -162,7 +162,7 @@ func (r *Recorder) Start() error { if r.started { return nil } - PromoteDraft(filepath.Dir(r.draftPath)) + DiscardDraft(filepath.Dir(r.draftPath)) err := os.MkdirAll(r.draftPath, 0o777) if err != nil { return E.Cause(err, "create power report draft directory") diff --git a/service/usbip/client.go b/service/usbip/client.go index ae49165e..231c447f 100644 --- a/service/usbip/client.go +++ b/service/usbip/client.go @@ -20,6 +20,7 @@ type ClientService struct { ctx context.Context logger log.ContextLogger inner *usbip.ClientService + detour string } func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, options option.USBIPClientServiceOptions) (adapter.Service, error) { @@ -47,6 +48,7 @@ func NewClientService(ctx context.Context, logger log.ContextLogger, tag string, ctx: ctx, logger: logger, inner: inner, + detour: options.Detour, }, nil } @@ -60,3 +62,10 @@ func (s *ClientService) Start(stage adapter.StartStage) error { func (s *ClientService) Close() error { return s.inner.Close() } + +func (s *ClientService) References() []string { + if s.detour == "" { + return nil + } + return []string{s.detour} +} diff --git a/transport/v2raygrpclite/client.go b/transport/v2raygrpclite/client.go index 7a42cf41..d8b3400e 100644 --- a/transport/v2raygrpclite/client.go +++ b/transport/v2raygrpclite/client.go @@ -126,6 +126,10 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { return conn, nil } +func (c *Client) CloseIdleConnections() { + c.transport.CloseIdleConnections() +} + func (c *Client) Close() error { v2rayhttp.ResetTransport(c.transport) return nil diff --git a/transport/v2rayhttp/client.go b/transport/v2rayhttp/client.go index bba65960..0b64c09f 100644 --- a/transport/v2rayhttp/client.go +++ b/transport/v2rayhttp/client.go @@ -170,6 +170,10 @@ func (c *Client) dialHTTP2(ctx context.Context) (net.Conn, error) { return conn, nil } +func (c *Client) CloseIdleConnections() { + CloseIdleConnections(c.transport) +} + func (c *Client) Close() error { c.transport.Store(ResetTransport(c.transport.Load())) return nil diff --git a/transport/v2rayquic/client.go b/transport/v2rayquic/client.go index 4e70e818..105785d5 100644 --- a/transport/v2rayquic/client.go +++ b/transport/v2rayquic/client.go @@ -28,10 +28,54 @@ type Client struct { tlsConfig tls.Config quicConfig *quic.Config connAccess sync.Mutex - conn common.TypedValue[*quic.Conn] + conn common.TypedValue[*clientConnection] rawConn net.Conn } +type clientConnection struct { + *quic.Conn + access sync.Mutex + streams int + draining bool + closed bool +} + +func (c *clientConnection) active() bool { + c.access.Lock() + closed := c.closed + c.access.Unlock() + return !closed && !common.Done(c.Context()) +} + +func (c *clientConnection) acquireStream() bool { + c.access.Lock() + defer c.access.Unlock() + if c.closed { + return false + } + c.streams++ + c.draining = false + return true +} + +func (c *clientConnection) releaseStream() { + c.access.Lock() + c.streams-- + closed := c.draining && c.streams == 0 && c.markClosedLocked() + c.access.Unlock() + if closed { + c.CloseWithError(0, "") + } +} + +func (c *clientConnection) markClosedLocked() bool { + if c.closed { + return false + } + c.closed = true + return true +} + func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { quicConfig := &quic.Config{ DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, @@ -48,15 +92,15 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt }, nil } -func (c *Client) offer() (*quic.Conn, error) { +func (c *Client) offer() (*clientConnection, error) { conn := c.conn.Load() - if conn != nil && !common.Done(conn.Context()) { + if conn != nil && conn.active() { return conn, nil } c.connAccess.Lock() defer c.connAccess.Unlock() conn = c.conn.Load() - if conn != nil && !common.Done(conn.Context()) { + if conn != nil && conn.active() { return conn, nil } conn, err := c.offerNew() @@ -66,7 +110,7 @@ func (c *Client) offer() (*quic.Conn, error) { return conn, nil } -func (c *Client) offerNew() (*quic.Conn, error) { +func (c *Client) offerNew() (*clientConnection, error) { udpConn, err := c.dialer.DialContext(c.ctx, "udp", c.serverAddr) if err != nil { return nil, err @@ -82,21 +126,42 @@ func (c *Client) offerNew() (*quic.Conn, error) { <-quicConn.Context().Done() udpConn.Close() }() - c.conn.Store(quicConn) + conn := &clientConnection{Conn: quicConn} + c.conn.Store(conn) c.rawConn = udpConn - return quicConn, nil + return conn, nil } func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { - conn, err := c.offer() - if err != nil { - return nil, err + for { + conn, err := c.offer() + if err != nil { + return nil, err + } + if !conn.acquireStream() { + continue + } + stream, err := conn.OpenStream() + if err != nil { + conn.releaseStream() + return nil, err + } + return &StreamWrapper{Conn: conn.Conn, Stream: stream, onClose: conn.releaseStream}, nil } - stream, err := conn.OpenStream() - if err != nil { - return nil, err +} + +func (c *Client) CloseIdleConnections() { + conn := c.conn.Load() + if conn == nil { + return + } + conn.access.Lock() + conn.draining = true + closed := conn.streams == 0 && conn.markClosedLocked() + conn.access.Unlock() + if closed { + conn.CloseWithError(0, "") } - return &StreamWrapper{Conn: conn, Stream: stream}, nil } func (c *Client) Close() error { diff --git a/transport/v2rayquic/stream.go b/transport/v2rayquic/stream.go index 411c57a4..7be6e2a9 100644 --- a/transport/v2rayquic/stream.go +++ b/transport/v2rayquic/stream.go @@ -2,6 +2,7 @@ package v2rayquic import ( "net" + "sync" "time" "github.com/sagernet/quic-go" @@ -11,6 +12,8 @@ import ( type StreamWrapper struct { Conn *quic.Conn *quic.Stream + closeOnce sync.Once + onClose func() } func (s *StreamWrapper) Read(p []byte) (n int, err error) { @@ -41,5 +44,8 @@ func (s *StreamWrapper) Close() error { // quic-go's Stream.Close does not unblock a Write blocked on flow control, // but a past write deadline does; buffered data and the FIN are unaffected. s.Stream.SetWriteDeadline(time.Now()) + if s.onClose != nil { + s.closeOnce.Do(s.onClose) + } return nil }