Close idle connections of unreferenced outbounds and DNS servers

This commit is contained in:
世界
2026-10-02 18:00:09 +08:00
parent d2cf9747d7
commit fd9a076d25
40 changed files with 851 additions and 81 deletions
+8
View File
@@ -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
}
+3 -3
View File
@@ -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(), "]")
}
+1
View File
@@ -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 {
+13 -9
View File
@@ -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)
},
+6 -4
View File
@@ -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 {
+26
View File
@@ -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
}
+15
View File
@@ -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
}
+1 -1
View File
@@ -67,7 +67,7 @@ func prepareWorkingDirectory() error {
return err
}
libbox.PromoteOOMDraft()
libbox.PromotePowerReportDraft()
libbox.DiscardPowerReportDraft()
return nil
}
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -36,6 +36,6 @@ func PowerReportOptions(startedService *daemon.StartedService) powerreport.Optio
}
}
func PromotePowerReportDraft() {
powerreport.PromoteDraft(sWorkingPath)
func DiscardPowerReportDraft() {
powerreport.DiscardDraft(sWorkingPath)
}
+3 -3
View File
@@ -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
+6 -6
View File
@@ -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=
+10 -3
View File
@@ -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)
+5
View File
@@ -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 {
+25 -1
View File
@@ -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()
}
}
+5
View File
@@ -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)
}
+5
View File
@@ -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)
}
+11 -1
View File
@@ -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))
}
+15 -4
View File
@@ -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
}
+9 -1
View File
@@ -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()
}
+60 -8
View File
@@ -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 {
+10
View File
@@ -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:
+8
View File
@@ -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
}
+15 -1
View File
@@ -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)
}
+8 -1
View File
@@ -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)
}
+16 -8
View File
@@ -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
}
+15 -1
View File
@@ -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)
}
+15 -1
View File
@@ -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)
}
+235
View File
@@ -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
}
+168
View File
@@ -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
}
+9
View File
@@ -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}
}
+6
View File
@@ -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
}))
}
+9
View File
@@ -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}
}
+3 -6
View File
@@ -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
+1 -1
View File
@@ -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")
+9
View File
@@ -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}
}
+4
View File
@@ -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
+4
View File
@@ -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
+79 -14
View File
@@ -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 {
+6
View File
@@ -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
}