Compare commits

..

15 Commits

Author SHA1 Message Date
世界
f56d9ab945 Bump version 2025-12-25 14:47:10 +08:00
世界
86fabd6a22 Update Mozilla certificates 2025-12-25 14:42:18 +08:00
世界
24a1e7cee4 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 14:40:48 +08:00
世界
223dd8bb1a Fix TCP DNS response buffer 2025-12-22 13:51:00 +08:00
世界
68448de7d0 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-22 13:50:57 +08:00
世界
1ebff74c21 Fix DNS cache not working when domain strategy is set
The cache lookup was performed before rule matching, using the caller's
strategy (usually AsIS/0) instead of the resolved strategy. This caused
cache misses when ipv4_only was configured globally but the cache lookup
expected both A and AAAA records.

Remove LookupCache and ExchangeCache from Router, as the cache checks
inside client.Lookup and client.Exchange already handle caching correctly
after rule matching with the proper strategy and transport.
2025-12-21 16:59:10 +08:00
世界
f0cd3422c1 Bump version 2025-12-14 00:09:19 +08:00
世界
e385a98ced Update Go to 1.25.5 2025-12-13 20:11:29 +08:00
世界
670f32baee Fix naive inbound 2025-12-12 21:19:28 +08:00
世界
2747a00ba2 Fix tailscale destination 2025-12-01 15:02:04 +08:00
世界
48e76038d0 Update Go to 1.25.4 2025-11-16 09:53:10 +08:00
世界
6421252d44 release: Fix windows7 build 2025-11-16 09:09:34 +08:00
世界
216c4c8bd4 Fix adapter handler 2025-11-16 08:34:46 +08:00
世界
5841d410a1 ssm-api: Fix save cache 2025-11-04 11:00:43 +08:00
Kumiko as a Service
63c8207d7a Use --no-cache --upgrade option in apk add
No need for separate upgrade / cache cleanup steps.

Signed-off-by: Kumiko as a Service <Dreista@users.noreply.github.com>
2025-11-04 11:00:41 +08:00
23 changed files with 1007 additions and 993 deletions

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
VERSION="1.25.3"
VERSION="1.25.5"
mkdir -p $HOME/go
cd $HOME/go

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -107,15 +107,15 @@ jobs:
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
go-version: ~1.24.10
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
@@ -123,7 +123,7 @@ jobs:
with:
path: |
~/go/go_win7
key: go_win7_1253
key: go_win7_1255
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |-
@@ -300,7 +300,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -380,7 +380,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -479,7 +479,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Set tag
if: matrix.if
run: |-

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
go-version: ~1.24.10
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:

View File

@@ -30,7 +30,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -71,7 +71,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.3
go-version: ^1.25.5
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1

View File

@@ -20,8 +20,6 @@ RUN set -ex \
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]

View File

@@ -27,8 +27,6 @@ type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache()
}

View File

@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
}
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx)
_, myMetadata := ExtendContext(ctx)
if source.IsValid() {
myMetadata.Source = source
}
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
}
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx)
_, myMetadata := ExtendContext(ctx)
if source.IsValid() {
myMetadata.Source = source
}
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
}
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
_, metadata := ExtendContext(ctx)
if source.IsValid() {
metadata.Source = source
}
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
}
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx)
_, metadata := ExtendContext(ctx)
if source.IsValid() {
metadata.Source = source
}

File diff suppressed because it is too large Load Diff

View File

@@ -353,68 +353,6 @@ func (c *Client) ClearCache() {
}
}
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
if c.disableCache || c.independentCache {
return nil, false
}
if dns.IsFqdn(domain) {
domain = domain[:len(domain)-1]
}
dnsName := dns.Fqdn(domain)
if strategy == C.DomainStrategyIPv4Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else if strategy == C.DomainStrategyIPv6Only {
addresses, err := c.questionCache(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if err != ErrNotCached {
return addresses, true
}
} else {
response4, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
}, nil)
if response4 == nil {
return nil, false
}
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response6 == nil {
return nil, false
}
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
}
return nil, false
}
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
if c.disableCache || c.independentCache || len(message.Question) != 1 {
return nil, false
}
question := message.Question[0]
response, ttl := c.loadResponse(question, nil)
if response == nil {
return nil, false
}
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, true
}
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...)

View File

@@ -214,97 +214,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
response *mDNS.Msg
transport adapter.DNSTransport
err error
)
response, cached := r.client.ExchangeCache(ctx, message)
if !cached {
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
options.Strategy = legacyTransport.LegacyStrategy()
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
}
if err != nil {
return nil, err
@@ -327,7 +325,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var (
responseAddrs []netip.Addr
cached bool
err error
)
printResult := func() {
@@ -347,13 +344,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain)
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}

View File

@@ -2,6 +2,21 @@
icon: material/alert-decagram
---
#### 1.12.14
* Fixes and improvements
#### 1.12.13
* Fix naive inbound
* Fixes and improvements
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
because system extensions require signatures to function, we have had to temporarily halt its release.__
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
#### 1.12.12
* Fixes and improvements

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! failure ""
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
## :material-graph: Requirements
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
* TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## :material-file-download: Download (macOS standalone version)
## ~~:material-file-download: Download (macOS standalone version)~~
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
```bash
brew install sfm
# brew install sfm
```
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
## :material-source-repository: Source code

2
go.mod
View File

@@ -27,7 +27,7 @@ require (
github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
github.com/sagernet/sing v0.7.13
github.com/sagernet/sing v0.7.14
github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
github.com/sagernet/sing-shadowsocks v0.2.8

4
go.sum
View File

@@ -167,8 +167,8 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.14/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=

View File

@@ -46,7 +46,8 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.DNSRouter, conn
conn.Close()
return err
}
responseBuffer := buf.NewPacket()
responseLength := response.Len()
responseBuffer := buf.NewSize(3 + responseLength)
defer responseBuffer.Release()
responseBuffer.Resize(2, 0)
n, err := response.PackBuffer(responseBuffer.FreeBytes())

View File

@@ -2,8 +2,8 @@ package naive
import (
"context"
"errors"
"io"
"math/rand"
"net"
"net/http"
@@ -22,7 +22,11 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
sHttp "github.com/sagernet/sing/protocol/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error)
@@ -82,16 +86,11 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
var tlsConfig *tls.STDConfig
if n.tlsConfig != nil {
err := n.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
tlsConfig, err = n.tlsConfig.Config()
if err != nil {
return err
}
}
if common.Contains(n.network, N.NetworkTCP) {
tcpListener, err := n.listener.ListenTCP()
@@ -99,20 +98,23 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
return err
}
n.httpServer = &http.Server{
Handler: n,
TLSConfig: tlsConfig,
Handler: h2c.NewHandler(n, &http2.Server{}),
BaseContext: func(listener net.Listener) context.Context {
return n.ctx
},
}
go func() {
var sErr error
if tlsConfig != nil {
sErr = n.httpServer.ServeTLS(tcpListener, "", "")
} else {
sErr = n.httpServer.Serve(tcpListener)
listener := net.Listener(tcpListener)
if n.tlsConfig != nil {
if len(n.tlsConfig.NextProtos()) == 0 {
n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
} else if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) {
n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...))
}
listener = aTLS.NewListener(tcpListener, n.tlsConfig)
}
if sErr != nil && !E.IsClosedOrCanceled(sErr) {
sErr := n.httpServer.Serve(listener)
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
n.logger.Error("http server serve error: ", sErr)
}
}()
@@ -161,13 +163,16 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
n.badRequest(ctx, request, E.New("authorization failed"))
return
}
writer.Header().Set("Padding", generateNaivePaddingHeader())
writer.Header().Set("Padding", generatePaddingHeader())
writer.WriteHeader(http.StatusOK)
writer.(http.Flusher).Flush()
hostPort := request.URL.Host
hostPort := request.Header.Get("-connect-authority")
if hostPort == "" {
hostPort = request.Host
hostPort = request.URL.Host
if hostPort == "" {
hostPort = request.Host
}
}
source := sHttp.SourceAddress(request)
destination := M.ParseSocksaddr(hostPort).Unwrap()
@@ -178,9 +183,14 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
n.badRequest(ctx, request, E.New("hijack failed"))
return
}
n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination)
} else {
n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
n.newConnection(ctx, true, &naiveH2Conn{
reader: request.Body,
writer: writer,
flusher: writer.(http.Flusher),
remoteAddress: source,
}, userName, source, destination)
}
}
@@ -236,18 +246,3 @@ func rejectHTTP(writer http.ResponseWriter, statusCode int) {
}
conn.Close()
}
func generateNaivePaddingHeader() string {
paddingLen := rand.Intn(32) + 30
padding := make([]byte, paddingLen)
bits := rand.Uint64()
for i := 0; i < 16; i++ {
// Codes that won't be Huffman coded.
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
bits >>= 4
}
for i := 16; i < paddingLen; i++ {
padding[i] = '~'
}
return string(padding)
}

View File

@@ -7,417 +7,242 @@ import (
"net"
"net/http"
"os"
"strings"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/baderror"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw"
)
const kFirstPaddings = 8
const paddingCount = 8
type naiveH1Conn struct {
net.Conn
func generatePaddingHeader() string {
paddingLen := rand.Intn(32) + 30
padding := make([]byte, paddingLen)
bits := rand.Uint64()
for i := 0; i < 16; i++ {
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
bits >>= 4
}
for i := 16; i < paddingLen; i++ {
padding[i] = '~'
}
return string(padding)
}
type paddingConn struct {
readPadding int
writePadding int
readRemaining int
paddingRemaining int
}
func (c *naiveH1Conn) Read(p []byte) (n int, err error) {
n, err = c.read(p)
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) {
if p.readRemaining > 0 {
if len(buffer) > p.readRemaining {
buffer = buffer[:p.readRemaining]
}
n, err = c.Conn.Read(p)
n, err = reader.Read(buffer)
if err != nil {
return
}
c.readRemaining -= n
p.readRemaining -= n
return
}
if c.paddingRemaining > 0 {
err = rw.SkipN(c.Conn, c.paddingRemaining)
if p.paddingRemaining > 0 {
err = rw.SkipN(reader, p.paddingRemaining)
if err != nil {
return
}
c.paddingRemaining = 0
p.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 3 {
paddingHdr = p[:3]
if p.readPadding < paddingCount {
var paddingHeader []byte
if len(buffer) >= 3 {
paddingHeader = buffer[:3]
} else {
paddingHdr = make([]byte, 3)
paddingHeader = make([]byte, 3)
}
_, err = io.ReadFull(c.Conn, paddingHdr)
_, err = io.ReadFull(reader, paddingHeader)
if err != nil {
return
}
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingSize := int(paddingHdr[2])
if len(p) > originalDataSize {
p = p[:originalDataSize]
originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2]))
paddingSize := int(paddingHeader[2])
if len(buffer) > originalDataSize {
buffer = buffer[:originalDataSize]
}
n, err = c.Conn.Read(p)
n, err = reader.Read(buffer)
if err != nil {
return
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingSize
p.readPadding++
p.readRemaining = originalDataSize - n
p.paddingRemaining = paddingSize
return
}
return c.Conn.Read(p)
return reader.Read(buffer)
}
func (c *naiveH1Conn) Write(p []byte) (n int, err error) {
for pLen := len(p); pLen > 0; {
var data []byte
if pLen > 65535 {
data = p[:65535]
p = p[65535:]
pLen -= 65535
} else {
data = p
pLen = 0
}
var writeN int
writeN, err = c.write(data)
n += writeN
if err != nil {
break
}
}
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) write(p []byte) (n int, err error) {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) {
if p.writePadding < paddingCount {
paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(p) + paddingSize)
buffer := buf.NewSize(3 + len(data) + paddingSize)
defer buffer.Release()
header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(p)))
binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(p))
_, err = c.Conn.Write(buffer.Bytes())
common.Must1(buffer.Write(data))
_, err = writer.Write(buffer.Bytes())
if err == nil {
n = len(p)
n = len(data)
}
c.writePadding++
p.writePadding++
return
}
return c.Conn.Write(p)
return writer.Write(data)
}
func (c *naiveH1Conn) FrontHeadroom() int {
if c.writePadding < kFirstPaddings {
return 3
}
return 0
}
func (c *naiveH1Conn) RearHeadroom() int {
if c.writePadding < kFirstPaddings {
return 255
}
return 0
}
func (c *naiveH1Conn) WriterMTU() int {
if c.writePadding < kFirstPaddings {
return 65535
}
return 0
}
func (c *naiveH1Conn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
if c.writePadding < kFirstPaddings {
func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error {
if p.writePadding < paddingCount {
bufferLen := buffer.Len()
if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes()))
_, err := p.writeChunked(writer, buffer.Bytes())
return err
}
paddingSize := rand.Intn(256)
header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(paddingSize)
buffer.Extend(paddingSize)
c.writePadding++
p.writePadding++
}
return wrapHttpError(common.Error(c.Conn.Write(buffer.Bytes())))
return common.Error(writer.Write(buffer.Bytes()))
}
// FIXME
/*func (c *naiveH1Conn) WriteTo(w io.Writer) (n int64, err error) {
if c.readPadding < kFirstPaddings {
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding)
} else {
n, err = bufio.Copy(w, c.Conn)
}
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) ReadFrom(r io.Reader) (n int64, err error) {
if c.writePadding < kFirstPaddings {
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
} else {
n, err = bufio.Copy(c.Conn, r)
}
return n, wrapHttpError(err)
}
*/
func (c *naiveH1Conn) Upstream() any {
return c.Conn
}
func (c *naiveH1Conn) ReaderReplaceable() bool {
return c.readPadding == kFirstPaddings
}
func (c *naiveH1Conn) WriterReplaceable() bool {
return c.writePadding == kFirstPaddings
}
type naiveH2Conn struct {
reader io.Reader
writer io.Writer
flusher http.Flusher
rAddr net.Addr
readPadding int
writePadding int
readRemaining int
paddingRemaining int
}
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.read(p)
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
}
n, err = c.reader.Read(p)
if err != nil {
return
}
c.readRemaining -= n
return
}
if c.paddingRemaining > 0 {
err = rw.SkipN(c.reader, c.paddingRemaining)
if err != nil {
return
}
c.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 3 {
paddingHdr = p[:3]
func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) {
for len(data) > 0 {
var chunk []byte
if len(data) > 65535 {
chunk = data[:65535]
data = data[65535:]
} else {
paddingHdr = make([]byte, 3)
chunk = data
data = nil
}
_, err = io.ReadFull(c.reader, paddingHdr)
var written int
written, err = p.writeWithPadding(writer, chunk)
n += written
if err != nil {
return
}
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingSize := int(paddingHdr[2])
if len(p) > originalDataSize {
p = p[:originalDataSize]
}
n, err = c.reader.Read(p)
if err != nil {
return
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingSize
return
}
return c.reader.Read(p)
return
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
for pLen := len(p); pLen > 0; {
var data []byte
if pLen > 65535 {
data = p[:65535]
p = p[65535:]
pLen -= 65535
} else {
data = p
pLen = 0
}
var writeN int
writeN, err = c.write(data)
n += writeN
if err != nil {
break
}
}
if err == nil {
c.flusher.Flush()
}
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) write(p []byte) (n int, err error) {
if c.writePadding < kFirstPaddings {
paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(p) + paddingSize)
defer buffer.Release()
header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(p)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(p))
_, err = c.writer.Write(buffer.Bytes())
if err == nil {
n = len(p)
}
c.writePadding++
return
}
return c.writer.Write(p)
}
func (c *naiveH2Conn) FrontHeadroom() int {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) frontHeadroom() int {
if p.writePadding < paddingCount {
return 3
}
return 0
}
func (c *naiveH2Conn) RearHeadroom() int {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) rearHeadroom() int {
if p.writePadding < paddingCount {
return 255
}
return 0
}
func (c *naiveH2Conn) WriterMTU() int {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) writerMTU() int {
if p.writePadding < paddingCount {
return 65535
}
return 0
}
func (p *paddingConn) readerReplaceable() bool {
return p.readPadding == paddingCount
}
func (p *paddingConn) writerReplaceable() bool {
return p.writePadding == paddingCount
}
type naiveConn struct {
net.Conn
paddingConn
}
func (c *naiveConn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.Conn, p)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.Conn, p)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
err := c.writeBufferWithPadding(c.Conn, buffer)
return baderror.WrapH2(err)
}
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
func (c *naiveConn) RearHeadroom() int { return c.rearHeadroom() }
func (c *naiveConn) WriterMTU() int { return c.writerMTU() }
func (c *naiveConn) Upstream() any { return c.Conn }
func (c *naiveConn) ReaderReplaceable() bool { return c.readerReplaceable() }
func (c *naiveConn) WriterReplaceable() bool { return c.writerReplaceable() }
type naiveH2Conn struct {
reader io.Reader
writer io.Writer
flusher http.Flusher
remoteAddress net.Addr
paddingConn
}
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.reader, p)
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.writer, p)
if err == nil {
c.flusher.Flush()
}
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
if c.writePadding < kFirstPaddings {
bufferLen := buffer.Len()
if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes()))
}
paddingSize := rand.Intn(256)
header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(paddingSize)
buffer.Extend(paddingSize)
c.writePadding++
}
err := common.Error(c.writer.Write(buffer.Bytes()))
err := c.writeBufferWithPadding(c.writer, buffer)
if err == nil {
c.flusher.Flush()
}
return wrapHttpError(err)
return baderror.WrapH2(err)
}
// FIXME
/*func (c *naiveH2Conn) WriteTo(w io.Writer) (n int64, err error) {
if c.readPadding < kFirstPaddings {
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding)
} else {
n, err = bufio.Copy(w, c.reader)
}
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) ReadFrom(r io.Reader) (n int64, err error) {
if c.writePadding < kFirstPaddings {
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
} else {
n, err = bufio.Copy(c.writer, r)
}
return n, wrapHttpError(err)
}*/
func (c *naiveH2Conn) Close() error {
return common.Close(
c.reader,
c.writer,
)
return common.Close(c.reader, c.writer)
}
func (c *naiveH2Conn) LocalAddr() net.Addr {
return M.Socksaddr{}
}
func (c *naiveH2Conn) RemoteAddr() net.Addr {
return c.rAddr
}
func (c *naiveH2Conn) SetDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *naiveH2Conn) UpstreamReader() any {
return c.reader
}
func (c *naiveH2Conn) UpstreamWriter() any {
return c.writer
}
func (c *naiveH2Conn) ReaderReplaceable() bool {
return c.readPadding == kFirstPaddings
}
func (c *naiveH2Conn) WriterReplaceable() bool {
return c.writePadding == kFirstPaddings
}
func wrapHttpError(err error) error {
if err == nil {
return err
}
if strings.Contains(err.Error(), "client disconnected") {
return net.ErrClosed
}
if strings.Contains(err.Error(), "body closed by handler") {
return net.ErrClosed
}
if strings.Contains(err.Error(), "canceled with error code 268") {
return io.EOF
}
return err
}
func (c *naiveH2Conn) LocalAddr() net.Addr { return M.Socksaddr{} }
func (c *naiveH2Conn) RemoteAddr() net.Addr { return c.remoteAddress }
func (c *naiveH2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid }
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid }
func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid }
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool { return true }
func (c *naiveH2Conn) UpstreamReader() any { return c.reader }
func (c *naiveH2Conn) UpstreamWriter() any { return c.writer }
func (c *naiveH2Conn) FrontHeadroom() int { return c.frontHeadroom() }
func (c *naiveH2Conn) RearHeadroom() int { return c.rearHeadroom() }
func (c *naiveH2Conn) WriterMTU() int { return c.writerMTU() }
func (c *naiveH2Conn) ReaderReplaceable() bool { return c.readerReplaceable() }
func (c *naiveH2Conn) WriterReplaceable() bool { return c.writerReplaceable() }

View File

@@ -38,6 +38,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/tailscale/ipn"
@@ -158,6 +159,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
},
TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(ctx),
Time: ntp.TimeFuncFromContext(ctx),
},
},
},
@@ -456,20 +458,20 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
metadata.Inbound = t.Tag()
metadata.InboundType = t.Type()
metadata.Source = source
metadata.Destination = destination
addr4, addr6 := t.server.TailscaleIPs()
switch destination.Addr {
case addr4:
metadata.OriginDestination = destination
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
case addr6:
metadata.OriginDestination = destination
destination.Addr = netip.IPv6Loopback()
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
}
metadata.Destination = destination
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -3,6 +3,7 @@ package derp
import (
"bufio"
"context"
stdTLS "crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -31,6 +32,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
@@ -159,6 +161,10 @@ func (d *Service) Start(stage adapter.StartStage) error {
httpClients = append(httpClients, &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSClientConfig: &stdTLS.Config{
RootCAs: adapter.RootPoolFromContext(d.ctx),
Time: ntp.TimeFuncFromContext(d.ctx),
},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},

View File

@@ -49,6 +49,9 @@ func (s *Service) loadCache() error {
os.RemoveAll(basePath)
return err
}
s.cacheMutex.Lock()
s.lastSavedCache = cacheBinary
s.cacheMutex.Unlock()
return nil
}
@@ -56,16 +59,30 @@ func (s *Service) saveCache() error {
if s.cachePath == "" {
return nil
}
cacheBinary, err := s.encodeCache()
if err != nil {
return err
}
s.cacheMutex.Lock()
defer s.cacheMutex.Unlock()
if bytes.Equal(s.lastSavedCache, cacheBinary) {
return nil
}
return s.writeCache(cacheBinary)
}
func (s *Service) writeCache(cacheBinary []byte) error {
basePath := filemanager.BasePath(s.ctx, s.cachePath)
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
if err != nil {
return err
}
cacheBinary, err := s.encodeCache()
err = os.WriteFile(basePath, cacheBinary, 0o644)
if err != nil {
return err
}
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
s.lastSavedCache = cacheBinary
return nil
}
func (s *Service) decodeCache(cacheBinary []byte) error {

View File

@@ -4,6 +4,8 @@ import (
"context"
"errors"
"net/http"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
@@ -28,21 +30,27 @@ func RegisterService(registry *boxService.Registry) {
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
traffics map[string]*TrafficManager
users map[string]*UserManager
cachePath string
ctx context.Context
cancel context.CancelFunc
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
traffics map[string]*TrafficManager
users map[string]*UserManager
cachePath string
saveTicker *time.Ticker
lastSavedCache []byte
cacheMutex sync.Mutex
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
ctx, cancel := context.WithCancel(ctx)
chiRouter := chi.NewRouter()
s := &Service{
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
ctx: ctx,
cancel: cancel,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
@@ -95,6 +103,8 @@ func (s *Service) Start(stage adapter.StartStage) error {
if err != nil {
s.logger.Error(E.Cause(err, "load cache"))
}
s.saveTicker = time.NewTicker(1 * time.Minute)
go s.loopSaveCache()
if s.tlsConfig != nil {
err = s.tlsConfig.Start()
if err != nil {
@@ -120,7 +130,27 @@ func (s *Service) Start(stage adapter.StartStage) error {
return nil
}
func (s *Service) loopSaveCache() {
for {
select {
case <-s.ctx.Done():
return
case <-s.saveTicker.C:
err := s.saveCache()
if err != nil {
s.logger.Error(E.Cause(err, "save cache"))
}
}
}
}
func (s *Service) Close() error {
if s.cancel != nil {
s.cancel()
}
if s.saveTicker != nil {
s.saveTicker.Stop()
}
err := s.saveCache()
if err != nil {
s.logger.Error(E.Cause(err, "save cache"))