From 0bc66e5a566a734d90dbd85a24e081b98a075704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 24 Feb 2026 19:16:40 +0800 Subject: [PATCH 01/12] service/ccm,ocm: Fixes and improvements --- go.mod | 4 +- go.sum | 12 +- service/ccm/service.go | 45 +- service/ccm/service_usage.go | 559 +++++++++++++++++-------- service/ocm/service.go | 91 +++- service/ocm/service_usage.go | 777 ++++++++++++++++++++++++++++++----- 6 files changed, 1201 insertions(+), 287 deletions(-) diff --git a/go.mod b/go.mod index db97d327..0523b73f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/sagernet/sing-box go 1.24.7 require ( - github.com/anthropics/anthropic-sdk-go v1.19.0 + github.com/anthropics/anthropic-sdk-go v1.26.0 github.com/anytls/sing-anytls v0.0.11 github.com/caddyserver/certmagic v0.25.0 github.com/coder/websocket v1.8.14 @@ -22,7 +22,7 @@ require ( github.com/metacubex/utls v1.8.4 github.com/mholt/acmez/v3 v3.1.4 github.com/miekg/dns v1.1.69 - github.com/openai/openai-go/v3 v3.15.0 + github.com/openai/openai-go/v3 v3.23.0 github.com/oschwald/maxminddb-golang v1.13.1 github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a diff --git a/go.sum b/go.sum index bb12cda4..738a415b 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/anthropics/anthropic-sdk-go v1.19.0 h1:mO6E+ffSzLRvR/YUH9KJC0uGw0uV8GjISIuzem//3KE= -github.com/anthropics/anthropic-sdk-go v1.19.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE= +github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY= +github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q= github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc= github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic= @@ -38,6 +38,8 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE= @@ -126,8 +128,8 @@ github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo= -github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.23.0 h1:FRFwTcB4FoWFtIunTY/8fgHvzSHgqbfWjiCwOMVrsvw= +github.com/openai/openai-go/v3 v3.23.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= @@ -378,6 +380,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/service/ccm/service.go b/service/ccm/service.go index 94e47734..ba428060 100644 --- a/service/ccm/service.go +++ b/service/ccm/service.go @@ -10,6 +10,7 @@ import ( "mime" "net" "net/http" + "strconv" "strings" "sync" "time" @@ -79,6 +80,35 @@ func isHopByHopHeader(header string) bool { } } +const ( + weeklyWindowSeconds = 604800 + weeklyWindowMinutes = weeklyWindowSeconds / 60 +) + +func parseInt64Header(headers http.Header, headerName string) (int64, bool) { + headerValue := strings.TrimSpace(headers.Get(headerName)) + if headerValue == "" { + return 0, false + } + parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) + if parseError != nil { + return 0, false + } + return parsedValue, true +} + +func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { + resetAtUnix, hasResetAt := parseInt64Header(headers, "anthropic-ratelimit-unified-7d-reset") + if !hasResetAt || resetAtUnix <= 0 { + return nil + } + + return &WeeklyCycleHint{ + WindowMinutes: weeklyWindowMinutes, + ResetAt: time.Unix(resetAtUnix, 0).UTC(), + } +} + type Service struct { boxService.Adapter ctx context.Context @@ -392,6 +422,7 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, requestModel string, anthropicBetaHeader string, messagesCount int, username string) { + weeklyCycleHint := extractWeeklyCycleHint(response.Header) mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) isStreaming := err == nil && mediaType == "text/event-stream" @@ -417,7 +448,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if usage.InputTokens > 0 || usage.OutputTokens > 0 { if responseModel != "" { contextWindow := detectContextWindow(anthropicBetaHeader, usage.InputTokens) - s.usageTracker.AddUsage( + s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, messagesCount, @@ -425,7 +456,11 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons usage.OutputTokens, usage.CacheReadInputTokens, usage.CacheCreationInputTokens, + usage.CacheCreation.Ephemeral5mInputTokens, + usage.CacheCreation.Ephemeral1hInputTokens, username, + time.Now(), + weeklyCycleHint, ) } } @@ -485,6 +520,8 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons accumulatedUsage.InputTokens = messageStart.Message.Usage.InputTokens accumulatedUsage.CacheReadInputTokens = messageStart.Message.Usage.CacheReadInputTokens accumulatedUsage.CacheCreationInputTokens = messageStart.Message.Usage.CacheCreationInputTokens + accumulatedUsage.CacheCreation.Ephemeral5mInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral5mInputTokens + accumulatedUsage.CacheCreation.Ephemeral1hInputTokens = messageStart.Message.Usage.CacheCreation.Ephemeral1hInputTokens } case "message_delta": messageDelta := event.AsMessageDelta() @@ -511,7 +548,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if accumulatedUsage.InputTokens > 0 || accumulatedUsage.OutputTokens > 0 { if responseModel != "" { contextWindow := detectContextWindow(anthropicBetaHeader, accumulatedUsage.InputTokens) - s.usageTracker.AddUsage( + s.usageTracker.AddUsageWithCycleHint( responseModel, contextWindow, messagesCount, @@ -519,7 +556,11 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons accumulatedUsage.OutputTokens, accumulatedUsage.CacheReadInputTokens, accumulatedUsage.CacheCreationInputTokens, + accumulatedUsage.CacheCreation.Ephemeral5mInputTokens, + accumulatedUsage.CacheCreation.Ephemeral1hInputTokens, username, + time.Now(), + weeklyCycleHint, ) } } diff --git a/service/ccm/service_usage.go b/service/ccm/service_usage.go index 7d39e3ce..7d776774 100644 --- a/service/ccm/service_usage.go +++ b/service/ccm/service_usage.go @@ -2,6 +2,7 @@ package ccm import ( "encoding/json" + "fmt" "math" "os" "regexp" @@ -13,17 +14,20 @@ import ( ) type UsageStats struct { - RequestCount int `json:"request_count"` - MessagesCount int `json:"messages_count"` - InputTokens int64 `json:"input_tokens"` - OutputTokens int64 `json:"output_tokens"` - CacheReadInputTokens int64 `json:"cache_read_input_tokens"` - CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + RequestCount int `json:"request_count"` + MessagesCount int `json:"messages_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` + CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` } type CostCombination struct { Model string `json:"model"` ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStats `json:"total"` ByUser map[string]UsageStats `json:"by_user"` } @@ -41,18 +45,21 @@ type AggregatedUsage struct { } type UsageStatsJSON struct { - RequestCount int `json:"request_count"` - MessagesCount int `json:"messages_count"` - InputTokens int64 `json:"input_tokens"` - OutputTokens int64 `json:"output_tokens"` - CacheReadInputTokens int64 `json:"cache_read_input_tokens"` - CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` - CostUSD float64 `json:"cost_usd"` + RequestCount int `json:"request_count"` + MessagesCount int `json:"messages_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheCreation5MinuteInputTokens int64 `json:"cache_creation_5m_input_tokens,omitempty"` + CacheCreation1HourInputTokens int64 `json:"cache_creation_1h_input_tokens,omitempty"` + CostUSD float64 `json:"cost_usd"` } type CostCombinationJSON struct { Model string `json:"model"` ContextWindow int `json:"context_window"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` Total UsageStatsJSON `json:"total"` ByUser map[string]UsageStatsJSON `json:"by_user"` } @@ -60,6 +67,7 @@ type CostCombinationJSON struct { type CostsSummaryJSON struct { TotalUSD float64 `json:"total_usd"` ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` } type AggregatedUsageJSON struct { @@ -68,11 +76,17 @@ type AggregatedUsageJSON struct { Combinations []CostCombinationJSON `json:"combinations"` } +type WeeklyCycleHint struct { + WindowMinutes int64 + ResetAt time.Time +} + type ModelPricing struct { - InputPrice float64 - OutputPrice float64 - CacheReadPrice float64 - CacheWritePrice float64 + InputPrice float64 + OutputPrice float64 + CacheReadPrice float64 + CacheWritePrice5Minute float64 + CacheWritePrice1Hour float64 } type modelFamily struct { @@ -82,143 +96,205 @@ type modelFamily struct { } var ( - opus4Pricing = ModelPricing{ - InputPrice: 15.0, - OutputPrice: 75.0, - CacheReadPrice: 1.5, - CacheWritePrice: 18.75, + opus46StandardPricing = ModelPricing{ + InputPrice: 5.0, + OutputPrice: 25.0, + CacheReadPrice: 0.5, + CacheWritePrice5Minute: 6.25, + CacheWritePrice1Hour: 10.0, } - sonnet4StandardPricing = ModelPricing{ - InputPrice: 3.0, - OutputPrice: 15.0, - CacheReadPrice: 0.3, - CacheWritePrice: 3.75, - } - - sonnet4PremiumPricing = ModelPricing{ - InputPrice: 6.0, - OutputPrice: 22.5, - CacheReadPrice: 0.6, - CacheWritePrice: 7.5, - } - - haiku4Pricing = ModelPricing{ - InputPrice: 1.0, - OutputPrice: 5.0, - CacheReadPrice: 0.1, - CacheWritePrice: 1.25, - } - - haiku35Pricing = ModelPricing{ - InputPrice: 0.8, - OutputPrice: 4.0, - CacheReadPrice: 0.08, - CacheWritePrice: 1.0, - } - - sonnet35Pricing = ModelPricing{ - InputPrice: 3.0, - OutputPrice: 15.0, - CacheReadPrice: 0.3, - CacheWritePrice: 3.75, + opus46PremiumPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 37.5, + CacheReadPrice: 1.0, + CacheWritePrice5Minute: 12.5, + CacheWritePrice1Hour: 20.0, } opus45Pricing = ModelPricing{ - InputPrice: 5.0, - OutputPrice: 25.0, - CacheReadPrice: 0.5, - CacheWritePrice: 6.25, + InputPrice: 5.0, + OutputPrice: 25.0, + CacheReadPrice: 0.5, + CacheWritePrice5Minute: 6.25, + CacheWritePrice1Hour: 10.0, + } + + opus4Pricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 75.0, + CacheReadPrice: 1.5, + CacheWritePrice5Minute: 18.75, + CacheWritePrice1Hour: 30.0, + } + + sonnet46StandardPricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet46PremiumPricing = ModelPricing{ + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, } sonnet45StandardPricing = ModelPricing{ - InputPrice: 3.0, - OutputPrice: 15.0, - CacheReadPrice: 0.3, - CacheWritePrice: 3.75, + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, } sonnet45PremiumPricing = ModelPricing{ - InputPrice: 6.0, - OutputPrice: 22.5, - CacheReadPrice: 0.6, - CacheWritePrice: 7.5, + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, + } + + sonnet4StandardPricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet4PremiumPricing = ModelPricing{ + InputPrice: 6.0, + OutputPrice: 22.5, + CacheReadPrice: 0.6, + CacheWritePrice5Minute: 7.5, + CacheWritePrice1Hour: 12.0, + } + + sonnet37Pricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, + } + + sonnet35Pricing = ModelPricing{ + InputPrice: 3.0, + OutputPrice: 15.0, + CacheReadPrice: 0.3, + CacheWritePrice5Minute: 3.75, + CacheWritePrice1Hour: 6.0, } haiku45Pricing = ModelPricing{ - InputPrice: 1.0, - OutputPrice: 5.0, - CacheReadPrice: 0.1, - CacheWritePrice: 1.25, + InputPrice: 1.0, + OutputPrice: 5.0, + CacheReadPrice: 0.1, + CacheWritePrice5Minute: 1.25, + CacheWritePrice1Hour: 2.0, + } + + haiku4Pricing = ModelPricing{ + InputPrice: 1.0, + OutputPrice: 5.0, + CacheReadPrice: 0.1, + CacheWritePrice5Minute: 1.25, + CacheWritePrice1Hour: 2.0, + } + + haiku35Pricing = ModelPricing{ + InputPrice: 0.8, + OutputPrice: 4.0, + CacheReadPrice: 0.08, + CacheWritePrice5Minute: 1.0, + CacheWritePrice1Hour: 1.6, } haiku3Pricing = ModelPricing{ - InputPrice: 0.25, - OutputPrice: 1.25, - CacheReadPrice: 0.03, - CacheWritePrice: 0.3, + InputPrice: 0.25, + OutputPrice: 1.25, + CacheReadPrice: 0.03, + CacheWritePrice5Minute: 0.3, + CacheWritePrice1Hour: 0.5, } opus3Pricing = ModelPricing{ - InputPrice: 15.0, - OutputPrice: 75.0, - CacheReadPrice: 1.5, - CacheWritePrice: 18.75, + InputPrice: 15.0, + OutputPrice: 75.0, + CacheReadPrice: 1.5, + CacheWritePrice5Minute: 18.75, + CacheWritePrice1Hour: 30.0, } modelFamilies = []modelFamily{ { - pattern: regexp.MustCompile(`^claude-opus-4-5-`), + pattern: regexp.MustCompile(`^claude-opus-4-6(?:-|$)`), + standardPricing: opus46StandardPricing, + premiumPricing: &opus46PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-opus-4-5(?:-|$)`), standardPricing: opus45Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-(?:opus-4-|4-opus-|opus-4-1-)`), + pattern: regexp.MustCompile(`^claude-(?:opus-4(?:-|$)|4-opus-)`), standardPricing: opus4Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-(?:opus-3-|3-opus-)`), + pattern: regexp.MustCompile(`^claude-(?:opus-3(?:-|$)|3-opus-)`), standardPricing: opus3Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5-|4-5-sonnet-)`), + pattern: regexp.MustCompile(`^claude-(?:sonnet-4-6(?:-|$)|4-6-sonnet-)`), + standardPricing: sonnet46StandardPricing, + premiumPricing: &sonnet46PremiumPricing, + }, + { + pattern: regexp.MustCompile(`^claude-(?:sonnet-4-5(?:-|$)|4-5-sonnet-)`), standardPricing: sonnet45StandardPricing, premiumPricing: &sonnet45PremiumPricing, }, { - pattern: regexp.MustCompile(`^claude-3-7-sonnet-`), + pattern: regexp.MustCompile(`^claude-(?:sonnet-4(?:-|$)|4-sonnet-)`), standardPricing: sonnet4StandardPricing, premiumPricing: &sonnet4PremiumPricing, }, { - pattern: regexp.MustCompile(`^claude-(?:sonnet-4-|4-sonnet-)`), - standardPricing: sonnet4StandardPricing, - premiumPricing: &sonnet4PremiumPricing, + pattern: regexp.MustCompile(`^claude-3-7-sonnet(?:-|$)`), + standardPricing: sonnet37Pricing, + premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-3-5-sonnet-`), + pattern: regexp.MustCompile(`^claude-3-5-sonnet(?:-|$)`), standardPricing: sonnet35Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-(?:haiku-4-5-|4-5-haiku-)`), + pattern: regexp.MustCompile(`^claude-(?:haiku-4-5(?:-|$)|4-5-haiku-)`), standardPricing: haiku45Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-haiku-4-`), + pattern: regexp.MustCompile(`^claude-haiku-4(?:-|$)`), standardPricing: haiku4Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-3-5-haiku-`), + pattern: regexp.MustCompile(`^claude-3-5-haiku(?:-|$)`), standardPricing: haiku35Pricing, premiumPricing: nil, }, { - pattern: regexp.MustCompile(`^claude-3-haiku-`), + pattern: regexp.MustCompile(`^claude-3-haiku(?:-|$)`), standardPricing: haiku3Pricing, premiumPricing: nil, }, @@ -243,68 +319,211 @@ func getPricing(model string, contextWindow int) ModelPricing { func calculateCost(stats UsageStats, model string, contextWindow int) float64 { pricing := getPricing(model, contextWindow) + cacheCreationCost := 0.0 + if stats.CacheCreation5MinuteInputTokens > 0 || stats.CacheCreation1HourInputTokens > 0 { + cacheCreationCost = float64(stats.CacheCreation5MinuteInputTokens)*pricing.CacheWritePrice5Minute + + float64(stats.CacheCreation1HourInputTokens)*pricing.CacheWritePrice1Hour + } else { + // Backward compatibility for usage files generated before TTL split tracking. + cacheCreationCost = float64(stats.CacheCreationInputTokens) * pricing.CacheWritePrice5Minute + } + cost := (float64(stats.InputTokens)*pricing.InputPrice + float64(stats.OutputTokens)*pricing.OutputPrice + float64(stats.CacheReadInputTokens)*pricing.CacheReadPrice + - float64(stats.CacheCreationInputTokens)*pricing.CacheWritePrice) / 1_000_000 + cacheCreationCost) / 1_000_000 return math.Round(cost*100) / 100 } +func roundCost(cost float64) float64 { + return math.Round(cost*100) / 100 +} + +func normalizeCombinations(combinations []CostCombination) { + for index := range combinations { + if combinations[index].ByUser == nil { + combinations[index].ByUser = make(map[string]UsageStats) + } + } +} + +func addUsageToCombinations( + combinations *[]CostCombination, + model string, + contextWindow int, + weekStartUnix int64, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, +) { + var matchedCombination *CostCombination + for index := range *combinations { + combination := &(*combinations)[index] + if combination.Model == model && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix { + matchedCombination = combination + break + } + } + + if matchedCombination == nil { + newCombination := CostCombination{ + Model: model, + ContextWindow: contextWindow, + WeekStartUnix: weekStartUnix, + Total: UsageStats{}, + ByUser: make(map[string]UsageStats), + } + *combinations = append(*combinations, newCombination) + matchedCombination = &(*combinations)[len(*combinations)-1] + } + + if cacheCreationTokens == 0 { + cacheCreationTokens = cacheCreation5MinuteTokens + cacheCreation1HourTokens + } + + matchedCombination.Total.RequestCount++ + matchedCombination.Total.MessagesCount += messagesCount + matchedCombination.Total.InputTokens += inputTokens + matchedCombination.Total.OutputTokens += outputTokens + matchedCombination.Total.CacheReadInputTokens += cacheReadTokens + matchedCombination.Total.CacheCreationInputTokens += cacheCreationTokens + matchedCombination.Total.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens + matchedCombination.Total.CacheCreation1HourInputTokens += cacheCreation1HourTokens + + if user != "" { + userStats := matchedCombination.ByUser[user] + userStats.RequestCount++ + userStats.MessagesCount += messagesCount + userStats.InputTokens += inputTokens + userStats.OutputTokens += outputTokens + userStats.CacheReadInputTokens += cacheReadTokens + userStats.CacheCreationInputTokens += cacheCreationTokens + userStats.CacheCreation5MinuteInputTokens += cacheCreation5MinuteTokens + userStats.CacheCreation1HourInputTokens += cacheCreation1HourTokens + matchedCombination.ByUser[user] = userStats + } +} + +func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { + result := make([]CostCombinationJSON, len(combinations)) + var totalCost float64 + + for index, combination := range combinations { + combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ContextWindow) + totalCost += combinationTotalCost + + combinationJSON := CostCombinationJSON{ + Model: combination.Model, + ContextWindow: combination.ContextWindow, + WeekStartUnix: combination.WeekStartUnix, + Total: UsageStatsJSON{ + RequestCount: combination.Total.RequestCount, + MessagesCount: combination.Total.MessagesCount, + InputTokens: combination.Total.InputTokens, + OutputTokens: combination.Total.OutputTokens, + CacheReadInputTokens: combination.Total.CacheReadInputTokens, + CacheCreationInputTokens: combination.Total.CacheCreationInputTokens, + CacheCreation5MinuteInputTokens: combination.Total.CacheCreation5MinuteInputTokens, + CacheCreation1HourInputTokens: combination.Total.CacheCreation1HourInputTokens, + CostUSD: combinationTotalCost, + }, + ByUser: make(map[string]UsageStatsJSON), + } + + for user, userStats := range combination.ByUser { + userCost := calculateCost(userStats, combination.Model, combination.ContextWindow) + if aggregateUserCosts != nil { + aggregateUserCosts[user] += userCost + } + + combinationJSON.ByUser[user] = UsageStatsJSON{ + RequestCount: userStats.RequestCount, + MessagesCount: userStats.MessagesCount, + InputTokens: userStats.InputTokens, + OutputTokens: userStats.OutputTokens, + CacheReadInputTokens: userStats.CacheReadInputTokens, + CacheCreationInputTokens: userStats.CacheCreationInputTokens, + CacheCreation5MinuteInputTokens: userStats.CacheCreation5MinuteInputTokens, + CacheCreation1HourInputTokens: userStats.CacheCreation1HourInputTokens, + CostUSD: userCost, + } + } + + result[index] = combinationJSON + } + + return result, roundCost(totalCost) +} + +func formatUTCOffsetLabel(timestamp time.Time) string { + _, offsetSeconds := timestamp.Zone() + sign := "+" + if offsetSeconds < 0 { + sign = "-" + offsetSeconds = -offsetSeconds + } + offsetHours := offsetSeconds / 3600 + offsetMinutes := (offsetSeconds % 3600) / 60 + if offsetMinutes == 0 { + return fmt.Sprintf("UTC%s%d", sign, offsetHours) + } + return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) +} + +func formatWeekStartKey(cycleStartAt time.Time) string { + localCycleStart := cycleStartAt.In(time.Local) + return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) +} + +func buildByWeekCost(combinations []CostCombination) map[string]float64 { + byWeek := make(map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ContextWindow) + } + for weekKey, weekCost := range byWeek { + byWeek[weekKey] = roundCost(weekCost) + } + return byWeek +} + +func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { + if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { + return 0 + } + windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute + return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() +} + func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { u.mutex.Lock() defer u.mutex.Unlock() result := &AggregatedUsageJSON{ - LastUpdated: u.LastUpdated, - Combinations: make([]CostCombinationJSON, len(u.Combinations)), + LastUpdated: u.LastUpdated, Costs: CostsSummaryJSON{ TotalUSD: 0, ByUser: make(map[string]float64), + ByWeek: make(map[string]float64), }, } - for i, combo := range u.Combinations { - totalCost := calculateCost(combo.Total, combo.Model, combo.ContextWindow) + globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) + result.Combinations = globalCombinationsJSON + result.Costs.TotalUSD = totalCost + result.Costs.ByWeek = buildByWeekCost(u.Combinations) - result.Costs.TotalUSD += totalCost - - comboJSON := CostCombinationJSON{ - Model: combo.Model, - ContextWindow: combo.ContextWindow, - Total: UsageStatsJSON{ - RequestCount: combo.Total.RequestCount, - MessagesCount: combo.Total.MessagesCount, - InputTokens: combo.Total.InputTokens, - OutputTokens: combo.Total.OutputTokens, - CacheReadInputTokens: combo.Total.CacheReadInputTokens, - CacheCreationInputTokens: combo.Total.CacheCreationInputTokens, - CostUSD: totalCost, - }, - ByUser: make(map[string]UsageStatsJSON), - } - - for user, userStats := range combo.ByUser { - userCost := calculateCost(userStats, combo.Model, combo.ContextWindow) - result.Costs.ByUser[user] += userCost - - comboJSON.ByUser[user] = UsageStatsJSON{ - RequestCount: userStats.RequestCount, - MessagesCount: userStats.MessagesCount, - InputTokens: userStats.InputTokens, - OutputTokens: userStats.OutputTokens, - CacheReadInputTokens: userStats.CacheReadInputTokens, - CacheCreationInputTokens: userStats.CacheCreationInputTokens, - CostUSD: userCost, - } - } - - result.Combinations[i] = comboJSON + if len(result.Costs.ByWeek) == 0 { + result.Costs.ByWeek = nil } - result.Costs.TotalUSD = math.Round(result.Costs.TotalUSD*100) / 100 for user, cost := range result.Costs.ByUser { - result.Costs.ByUser[user] = math.Round(cost*100) / 100 + result.Costs.ByUser[user] = roundCost(cost) } return result @@ -314,6 +533,9 @@ func (u *AggregatedUsage) Load() error { u.mutex.Lock() defer u.mutex.Unlock() + u.LastUpdated = time.Time{} + u.Combinations = nil + data, err := os.ReadFile(u.filePath) if err != nil { if os.IsNotExist(err) { @@ -334,12 +556,7 @@ func (u *AggregatedUsage) Load() error { u.LastUpdated = temp.LastUpdated u.Combinations = temp.Combinations - - for i := range u.Combinations { - if u.Combinations[i].ByUser == nil { - u.Combinations[i].ByUser = make(map[string]UsageStats) - } - } + normalizeCombinations(u.Combinations) return nil } @@ -367,58 +584,42 @@ func (u *AggregatedUsage) Save() error { return err } -func (u *AggregatedUsage) AddUsage(model string, contextWindow int, messagesCount int, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens int64, user string) error { +func (u *AggregatedUsage) AddUsage( + model string, + contextWindow int, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, +) error { + return u.AddUsageWithCycleHint(model, contextWindow, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user, time.Now(), nil) +} + +func (u *AggregatedUsage) AddUsageWithCycleHint( + model string, + contextWindow int, + messagesCount int, + inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens int64, + user string, + observedAt time.Time, + cycleHint *WeeklyCycleHint, +) error { if model == "" { return E.New("model cannot be empty") } if contextWindow <= 0 { return E.New("contextWindow must be positive") } + if observedAt.IsZero() { + observedAt = time.Now() + } u.mutex.Lock() defer u.mutex.Unlock() - u.LastUpdated = time.Now() + u.LastUpdated = observedAt + weekStartUnix := deriveWeekStartUnix(cycleHint) - // Find or create combination - var combo *CostCombination - for i := range u.Combinations { - if u.Combinations[i].Model == model && u.Combinations[i].ContextWindow == contextWindow { - combo = &u.Combinations[i] - break - } - } - - if combo == nil { - newCombo := CostCombination{ - Model: model, - ContextWindow: contextWindow, - Total: UsageStats{}, - ByUser: make(map[string]UsageStats), - } - u.Combinations = append(u.Combinations, newCombo) - combo = &u.Combinations[len(u.Combinations)-1] - } - - // Update total stats - combo.Total.RequestCount++ - combo.Total.MessagesCount += messagesCount - combo.Total.InputTokens += inputTokens - combo.Total.OutputTokens += outputTokens - combo.Total.CacheReadInputTokens += cacheReadTokens - combo.Total.CacheCreationInputTokens += cacheCreationTokens - - // Update per-user stats if user is specified - if user != "" { - userStats := combo.ByUser[user] - userStats.RequestCount++ - userStats.MessagesCount += messagesCount - userStats.InputTokens += inputTokens - userStats.OutputTokens += outputTokens - userStats.CacheReadInputTokens += cacheReadTokens - userStats.CacheCreationInputTokens += cacheCreationTokens - combo.ByUser[user] = userStats - } + addUsageToCombinations(&u.Combinations, model, contextWindow, weekStartUnix, messagesCount, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, cacheCreation5MinuteTokens, cacheCreation1HourTokens, user) go u.scheduleSave() diff --git a/service/ocm/service.go b/service/ocm/service.go index fc655f67..2354d159 100644 --- a/service/ocm/service.go +++ b/service/ocm/service.go @@ -10,6 +10,7 @@ import ( "mime" "net" "net/http" + "strconv" "strings" "sync" "time" @@ -71,6 +72,57 @@ func isHopByHopHeader(header string) bool { } } +func normalizeRateLimitIdentifier(limitIdentifier string) string { + trimmedIdentifier := strings.TrimSpace(strings.ToLower(limitIdentifier)) + if trimmedIdentifier == "" { + return "" + } + return strings.ReplaceAll(trimmedIdentifier, "_", "-") +} + +func parseInt64Header(headers http.Header, headerName string) (int64, bool) { + headerValue := strings.TrimSpace(headers.Get(headerName)) + if headerValue == "" { + return 0, false + } + parsedValue, parseError := strconv.ParseInt(headerValue, 10, 64) + if parseError != nil { + return 0, false + } + return parsedValue, true +} + +func weeklyCycleHintForLimit(headers http.Header, limitIdentifier string) *WeeklyCycleHint { + normalizedLimitIdentifier := normalizeRateLimitIdentifier(limitIdentifier) + if normalizedLimitIdentifier == "" { + return nil + } + + windowHeader := "x-" + normalizedLimitIdentifier + "-secondary-window-minutes" + resetHeader := "x-" + normalizedLimitIdentifier + "-secondary-reset-at" + + windowMinutes, hasWindowMinutes := parseInt64Header(headers, windowHeader) + resetAtUnix, hasResetAt := parseInt64Header(headers, resetHeader) + if !hasWindowMinutes || !hasResetAt || windowMinutes <= 0 || resetAtUnix <= 0 { + return nil + } + + return &WeeklyCycleHint{ + WindowMinutes: windowMinutes, + ResetAt: time.Unix(resetAtUnix, 0).UTC(), + } +} + +func extractWeeklyCycleHint(headers http.Header) *WeeklyCycleHint { + activeLimitIdentifier := normalizeRateLimitIdentifier(headers.Get("x-codex-active-limit")) + if activeLimitIdentifier != "" { + if activeHint := weeklyCycleHintForLimit(headers, activeLimitIdentifier); activeHint != nil { + return activeHint + } + } + return weeklyCycleHintForLimit(headers, "codex") +} + type Service struct { boxService.Adapter ctx context.Context @@ -404,9 +456,12 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, response *http.Response, path string, requestModel string, username string) { isChatCompletions := path == "/v1/chat/completions" + weeklyCycleHint := extractWeeklyCycleHint(response.Header) mediaType, _, err := mime.ParseMediaType(response.Header.Get("Content-Type")) isStreaming := err == nil && mediaType == "text/event-stream" - + if !isStreaming && !isChatCompletions && response.Header.Get("Content-Type") == "" { + isStreaming = true + } if !isStreaming { bodyBytes, err := io.ReadAll(response.Body) if err != nil { @@ -414,13 +469,14 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons return } - var responseModel string + var responseModel, serviceTier string var inputTokens, outputTokens, cachedTokens int64 if isChatCompletions { var chatCompletion openai.ChatCompletion if json.Unmarshal(bodyBytes, &chatCompletion) == nil { responseModel = chatCompletion.Model + serviceTier = string(chatCompletion.ServiceTier) inputTokens = chatCompletion.Usage.PromptTokens outputTokens = chatCompletion.Usage.CompletionTokens cachedTokens = chatCompletion.Usage.PromptTokensDetails.CachedTokens @@ -429,6 +485,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons var responsesResponse responses.Response if json.Unmarshal(bodyBytes, &responsesResponse) == nil { responseModel = string(responsesResponse.Model) + serviceTier = string(responsesResponse.ServiceTier) inputTokens = responsesResponse.Usage.InputTokens outputTokens = responsesResponse.Usage.OutputTokens cachedTokens = responsesResponse.Usage.InputTokensDetails.CachedTokens @@ -440,7 +497,16 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons responseModel = requestModel } if responseModel != "" { - s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, username) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) } } @@ -455,7 +521,7 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons } var inputTokens, outputTokens, cachedTokens int64 - var responseModel string + var responseModel, serviceTier string buffer := make([]byte, buf.BufferSize) var leftover []byte @@ -490,6 +556,9 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if chatChunk.Model != "" { responseModel = chatChunk.Model } + if chatChunk.ServiceTier != "" { + serviceTier = string(chatChunk.ServiceTier) + } if chatChunk.Usage.PromptTokens > 0 { inputTokens = chatChunk.Usage.PromptTokens cachedTokens = chatChunk.Usage.PromptTokensDetails.CachedTokens @@ -506,6 +575,9 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if string(completedEvent.Response.Model) != "" { responseModel = string(completedEvent.Response.Model) } + if completedEvent.Response.ServiceTier != "" { + serviceTier = string(completedEvent.Response.ServiceTier) + } if completedEvent.Response.Usage.InputTokens > 0 { inputTokens = completedEvent.Response.Usage.InputTokens cachedTokens = completedEvent.Response.Usage.InputTokensDetails.CachedTokens @@ -534,7 +606,16 @@ func (s *Service) handleResponseWithTracking(writer http.ResponseWriter, respons if inputTokens > 0 || outputTokens > 0 { if responseModel != "" { - s.usageTracker.AddUsage(responseModel, inputTokens, outputTokens, cachedTokens, username) + s.usageTracker.AddUsageWithCycleHint( + responseModel, + inputTokens, + outputTokens, + cachedTokens, + serviceTier, + username, + time.Now(), + weeklyCycleHint, + ) } } return diff --git a/service/ocm/service_usage.go b/service/ocm/service_usage.go index 7089f4d3..a4c1d1c8 100644 --- a/service/ocm/service_usage.go +++ b/service/ocm/service_usage.go @@ -2,9 +2,11 @@ package ocm import ( "encoding/json" + "fmt" "math" "os" "regexp" + "strings" "sync" "time" @@ -42,9 +44,11 @@ func (u *UsageStats) UnmarshalJSON(data []byte) error { } type CostCombination struct { - Model string `json:"model"` - Total UsageStats `json:"total"` - ByUser map[string]UsageStats `json:"by_user"` + Model string `json:"model"` + ServiceTier string `json:"service_tier,omitempty"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStats `json:"total"` + ByUser map[string]UsageStats `json:"by_user"` } type AggregatedUsage struct { @@ -68,14 +72,17 @@ type UsageStatsJSON struct { } type CostCombinationJSON struct { - Model string `json:"model"` - Total UsageStatsJSON `json:"total"` - ByUser map[string]UsageStatsJSON `json:"by_user"` + Model string `json:"model"` + ServiceTier string `json:"service_tier,omitempty"` + WeekStartUnix int64 `json:"week_start_unix,omitempty"` + Total UsageStatsJSON `json:"total"` + ByUser map[string]UsageStatsJSON `json:"by_user"` } type CostsSummaryJSON struct { TotalUSD float64 `json:"total_usd"` ByUser map[string]float64 `json:"by_user"` + ByWeek map[string]float64 `json:"by_week,omitempty"` } type AggregatedUsageJSON struct { @@ -84,6 +91,11 @@ type AggregatedUsageJSON struct { Combinations []CostCombinationJSON `json:"combinations"` } +type WeeklyCycleHint struct { + WindowMinutes int64 + ResetAt time.Time +} + type ModelPricing struct { InputPrice float64 OutputPrice float64 @@ -95,7 +107,123 @@ type modelFamily struct { pricing ModelPricing } +const ( + serviceTierAuto = "auto" + serviceTierDefault = "default" + serviceTierFlex = "flex" + serviceTierPriority = "priority" + serviceTierScale = "scale" +) + var ( + gpt52Pricing = ModelPricing{ + InputPrice: 1.75, + OutputPrice: 14.0, + CachedInputPrice: 0.175, + } + + gpt5Pricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 10.0, + CachedInputPrice: 0.125, + } + + gpt5MiniPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 2.0, + CachedInputPrice: 0.025, + } + + gpt5NanoPricing = ModelPricing{ + InputPrice: 0.05, + OutputPrice: 0.4, + CachedInputPrice: 0.005, + } + + gpt52CodexPricing = ModelPricing{ + InputPrice: 1.75, + OutputPrice: 14.0, + CachedInputPrice: 0.175, + } + + gpt51CodexPricing = ModelPricing{ + InputPrice: 1.25, + OutputPrice: 10.0, + CachedInputPrice: 0.125, + } + + gpt51CodexMiniPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 2.0, + CachedInputPrice: 0.025, + } + + gpt52ProPricing = ModelPricing{ + InputPrice: 21.0, + OutputPrice: 168.0, + CachedInputPrice: 21.0, + } + + gpt5ProPricing = ModelPricing{ + InputPrice: 15.0, + OutputPrice: 120.0, + CachedInputPrice: 15.0, + } + + gpt52FlexPricing = ModelPricing{ + InputPrice: 0.875, + OutputPrice: 7.0, + CachedInputPrice: 0.0875, + } + + gpt5FlexPricing = ModelPricing{ + InputPrice: 0.625, + OutputPrice: 5.0, + CachedInputPrice: 0.0625, + } + + gpt5MiniFlexPricing = ModelPricing{ + InputPrice: 0.125, + OutputPrice: 1.0, + CachedInputPrice: 0.0125, + } + + gpt5NanoFlexPricing = ModelPricing{ + InputPrice: 0.025, + OutputPrice: 0.2, + CachedInputPrice: 0.0025, + } + + gpt52PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 28.0, + CachedInputPrice: 0.35, + } + + gpt5PriorityPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 20.0, + CachedInputPrice: 0.25, + } + + gpt5MiniPriorityPricing = ModelPricing{ + InputPrice: 0.45, + OutputPrice: 3.6, + CachedInputPrice: 0.045, + } + + gpt52CodexPriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 28.0, + CachedInputPrice: 0.35, + } + + gpt51CodexPriorityPricing = ModelPricing{ + InputPrice: 2.5, + OutputPrice: 20.0, + CachedInputPrice: 0.25, + } + gpt4oPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 10.0, @@ -111,7 +239,19 @@ var ( gpt4oAudioPricing = ModelPricing{ InputPrice: 2.5, OutputPrice: 10.0, - CachedInputPrice: 1.25, + CachedInputPrice: 2.5, + } + + gpt4oMiniAudioPricing = ModelPricing{ + InputPrice: 0.15, + OutputPrice: 0.6, + CachedInputPrice: 0.15, + } + + gptAudioMiniPricing = ModelPricing{ + InputPrice: 0.6, + OutputPrice: 2.4, + CachedInputPrice: 0.6, } o1Pricing = ModelPricing{ @@ -120,6 +260,12 @@ var ( CachedInputPrice: 7.5, } + o1ProPricing = ModelPricing{ + InputPrice: 150.0, + OutputPrice: 600.0, + CachedInputPrice: 150.0, + } + o1MiniPricing = ModelPricing{ InputPrice: 1.1, OutputPrice: 4.4, @@ -135,13 +281,55 @@ var ( o3Pricing = ModelPricing{ InputPrice: 2.0, OutputPrice: 8.0, - CachedInputPrice: 1.0, + CachedInputPrice: 0.5, + } + + o3ProPricing = ModelPricing{ + InputPrice: 20.0, + OutputPrice: 80.0, + CachedInputPrice: 20.0, + } + + o3DeepResearchPricing = ModelPricing{ + InputPrice: 10.0, + OutputPrice: 40.0, + CachedInputPrice: 2.5, } o4MiniPricing = ModelPricing{ InputPrice: 1.1, OutputPrice: 4.4, - CachedInputPrice: 0.55, + CachedInputPrice: 0.275, + } + + o4MiniDeepResearchPricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, + } + + o3FlexPricing = ModelPricing{ + InputPrice: 1.0, + OutputPrice: 4.0, + CachedInputPrice: 0.25, + } + + o4MiniFlexPricing = ModelPricing{ + InputPrice: 0.55, + OutputPrice: 2.2, + CachedInputPrice: 0.138, + } + + o3PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 14.0, + CachedInputPrice: 0.875, + } + + o4MiniPriorityPricing = ModelPricing{ + InputPrice: 2.0, + OutputPrice: 8.0, + CachedInputPrice: 0.5, } gpt41Pricing = ModelPricing{ @@ -162,69 +350,374 @@ var ( CachedInputPrice: 0.025, } - modelFamilies = []modelFamily{ + gpt41PriorityPricing = ModelPricing{ + InputPrice: 3.5, + OutputPrice: 14.0, + CachedInputPrice: 0.875, + } + + gpt41MiniPriorityPricing = ModelPricing{ + InputPrice: 0.7, + OutputPrice: 2.8, + CachedInputPrice: 0.175, + } + + gpt41NanoPriorityPricing = ModelPricing{ + InputPrice: 0.2, + OutputPrice: 0.8, + CachedInputPrice: 0.05, + } + + gpt4oPriorityPricing = ModelPricing{ + InputPrice: 4.25, + OutputPrice: 17.0, + CachedInputPrice: 2.125, + } + + gpt4oMiniPriorityPricing = ModelPricing{ + InputPrice: 0.25, + OutputPrice: 1.0, + CachedInputPrice: 0.125, + } + + standardModelFamilies = []modelFamily{ { - pattern: regexp.MustCompile(`^gpt-4\.1-nano`), - pricing: gpt41NanoPricing, + pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), + pricing: gpt52CodexPricing, }, { - pattern: regexp.MustCompile(`^gpt-4\.1-mini`), - pricing: gpt41MiniPricing, + pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), + pricing: gpt52CodexPricing, }, { - pattern: regexp.MustCompile(`^gpt-4\.1`), - pricing: gpt41Pricing, + pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), + pricing: gpt51CodexPricing, }, { - pattern: regexp.MustCompile(`^o4-mini`), + pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`), + pricing: gpt51CodexMiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), + pricing: gpt51CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), + pricing: gpt51CodexMiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), + pricing: gpt51CodexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`), + pricing: gpt52Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-chat-latest$`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`), + pricing: gpt52ProPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`), + pricing: gpt5ProPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), + pricing: gpt5NanoPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5Pricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`), + pricing: o4MiniDeepResearchPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), pricing: o4MiniPricing, }, { - pattern: regexp.MustCompile(`^o3-mini`), + pattern: regexp.MustCompile(`^o3-pro(?:$|-)`), + pricing: o3ProPricing, + }, + { + pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`), + pricing: o3DeepResearchPricing, + }, + { + pattern: regexp.MustCompile(`^o3-mini(?:$|-)`), pricing: o3MiniPricing, }, { - pattern: regexp.MustCompile(`^o3`), + pattern: regexp.MustCompile(`^o3(?:$|-)`), pricing: o3Pricing, }, { - pattern: regexp.MustCompile(`^o1-mini`), + pattern: regexp.MustCompile(`^o1-pro(?:$|-)`), + pricing: o1ProPricing, + }, + { + pattern: regexp.MustCompile(`^o1-mini(?:$|-)`), pricing: o1MiniPricing, }, { - pattern: regexp.MustCompile(`^o1`), + pattern: regexp.MustCompile(`^o1(?:$|-)`), pricing: o1Pricing, }, { - pattern: regexp.MustCompile(`^gpt-4o-audio`), + pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`), + pricing: gpt4oMiniAudioPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`), + pricing: gptAudioMiniPricing, + }, + { + pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`), pricing: gpt4oAudioPricing, }, { - pattern: regexp.MustCompile(`^gpt-4o-mini`), + pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), + pricing: gpt41NanoPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), + pricing: gpt41MiniPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), + pricing: gpt41Pricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), pricing: gpt4oMiniPricing, }, { - pattern: regexp.MustCompile(`^gpt-4o`), + pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), pricing: gpt4oPricing, }, { - pattern: regexp.MustCompile(`^chatgpt-4o`), + pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`), pricing: gpt4oPricing, }, } + + flexModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`), + pricing: gpt5NanoFlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52FlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5FlexPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5FlexPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), + pricing: o4MiniFlexPricing, + }, + { + pattern: regexp.MustCompile(`^o3(?:$|-)`), + pricing: o3FlexPricing, + }, + } + + priorityModelFamilies = []modelFamily{ + { + pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`), + pricing: gpt52CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`), + pricing: gpt52CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`), + pricing: gpt5MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`), + pricing: gpt51CodexPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`), + pricing: gpt5MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`), + pricing: gpt52PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`), + pricing: gpt5PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-5(?:$|-)`), + pricing: gpt5PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^o4-mini(?:$|-)`), + pricing: o4MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^o3(?:$|-)`), + pricing: o3PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`), + pricing: gpt41NanoPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`), + pricing: gpt41MiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`), + pricing: gpt41PriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`), + pricing: gpt4oMiniPriorityPricing, + }, + { + pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`), + pricing: gpt4oPriorityPricing, + }, + } ) -func getPricing(model string) ModelPricing { +func modelFamiliesForTier(serviceTier string) []modelFamily { + switch serviceTier { + case serviceTierFlex: + return flexModelFamilies + case serviceTierPriority: + return priorityModelFamilies + default: + return standardModelFamilies + } +} + +func findPricingInFamilies(model string, modelFamilies []modelFamily) (ModelPricing, bool) { for _, family := range modelFamilies { if family.pattern.MatchString(model) { - return family.pricing + return family.pricing, true } } + return ModelPricing{}, false +} + +func normalizeServiceTier(serviceTier string) string { + switch strings.ToLower(strings.TrimSpace(serviceTier)) { + case "", serviceTierAuto, serviceTierDefault: + return serviceTierDefault + case serviceTierFlex: + return serviceTierFlex + case serviceTierPriority: + return serviceTierPriority + case serviceTierScale: + // Scale-tier requests are prepaid differently and not listed in this usage file. + return serviceTierDefault + default: + return serviceTierDefault + } +} + +func getPricing(model string, serviceTier string) ModelPricing { + normalizedServiceTier := normalizeServiceTier(serviceTier) + modelFamilies := modelFamiliesForTier(normalizedServiceTier) + + if pricing, found := findPricingInFamilies(model, modelFamilies); found { + return pricing + } + + normalizedModel := normalizeGPT5Model(model) + if normalizedModel != model { + if pricing, found := findPricingInFamilies(normalizedModel, modelFamilies); found { + return pricing + } + } + + if normalizedServiceTier != serviceTierDefault { + if pricing, found := findPricingInFamilies(model, standardModelFamilies); found { + return pricing + } + if normalizedModel != model { + if pricing, found := findPricingInFamilies(normalizedModel, standardModelFamilies); found { + return pricing + } + } + } + return gpt4oPricing } -func calculateCost(stats UsageStats, model string) float64 { - pricing := getPricing(model) +func normalizeGPT5Model(model string) string { + if !strings.HasPrefix(model, "gpt-5.") { + return model + } + + switch { + case strings.Contains(model, "-codex-mini"): + return "gpt-5.1-codex-mini" + case strings.Contains(model, "-codex-max"): + return "gpt-5.1-codex-max" + case strings.Contains(model, "-codex"): + return "gpt-5.3-codex" + case strings.Contains(model, "-chat-latest"): + return "gpt-5.2-chat-latest" + case strings.Contains(model, "-pro"): + return "gpt-5.2-pro" + case strings.Contains(model, "-mini"): + return "gpt-5-mini" + case strings.Contains(model, "-nano"): + return "gpt-5-nano" + default: + return "gpt-5.2" + } +} + +func calculateCost(stats UsageStats, model string, serviceTier string) float64 { + pricing := getPricing(model, serviceTier) regularInputTokens := stats.InputTokens - stats.CachedTokens if regularInputTokens < 0 { @@ -238,41 +731,89 @@ func calculateCost(stats UsageStats, model string) float64 { return math.Round(cost*100) / 100 } -func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { - u.mutex.Lock() - defer u.mutex.Unlock() +func roundCost(cost float64) float64 { + return math.Round(cost*100) / 100 +} - result := &AggregatedUsageJSON{ - LastUpdated: u.LastUpdated, - Combinations: make([]CostCombinationJSON, len(u.Combinations)), - Costs: CostsSummaryJSON{ - TotalUSD: 0, - ByUser: make(map[string]float64), - }, +func normalizeCombinations(combinations []CostCombination) { + for index := range combinations { + combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier) + if combinations[index].ByUser == nil { + combinations[index].ByUser = make(map[string]UsageStats) + } + } +} + +func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) { + var matchedCombination *CostCombination + for index := range *combinations { + combination := &(*combinations)[index] + combinationServiceTier := normalizeServiceTier(combination.ServiceTier) + if combination.ServiceTier != combinationServiceTier { + combination.ServiceTier = combinationServiceTier + } + if combination.Model == model && combinationServiceTier == serviceTier && combination.WeekStartUnix == weekStartUnix { + matchedCombination = combination + break + } } - for i, combo := range u.Combinations { - totalCost := calculateCost(combo.Total, combo.Model) + if matchedCombination == nil { + newCombination := CostCombination{ + Model: model, + ServiceTier: serviceTier, + WeekStartUnix: weekStartUnix, + Total: UsageStats{}, + ByUser: make(map[string]UsageStats), + } + *combinations = append(*combinations, newCombination) + matchedCombination = &(*combinations)[len(*combinations)-1] + } - result.Costs.TotalUSD += totalCost + matchedCombination.Total.RequestCount++ + matchedCombination.Total.InputTokens += inputTokens + matchedCombination.Total.OutputTokens += outputTokens + matchedCombination.Total.CachedTokens += cachedTokens - comboJSON := CostCombinationJSON{ - Model: combo.Model, + if user != "" { + userStats := matchedCombination.ByUser[user] + userStats.RequestCount++ + userStats.InputTokens += inputTokens + userStats.OutputTokens += outputTokens + userStats.CachedTokens += cachedTokens + matchedCombination.ByUser[user] = userStats + } +} + +func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) { + result := make([]CostCombinationJSON, len(combinations)) + var totalCost float64 + + for index, combination := range combinations { + combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier) + totalCost += combinationTotalCost + + combinationJSON := CostCombinationJSON{ + Model: combination.Model, + ServiceTier: combination.ServiceTier, + WeekStartUnix: combination.WeekStartUnix, Total: UsageStatsJSON{ - RequestCount: combo.Total.RequestCount, - InputTokens: combo.Total.InputTokens, - OutputTokens: combo.Total.OutputTokens, - CachedTokens: combo.Total.CachedTokens, - CostUSD: totalCost, + RequestCount: combination.Total.RequestCount, + InputTokens: combination.Total.InputTokens, + OutputTokens: combination.Total.OutputTokens, + CachedTokens: combination.Total.CachedTokens, + CostUSD: combinationTotalCost, }, ByUser: make(map[string]UsageStatsJSON), } - for user, userStats := range combo.ByUser { - userCost := calculateCost(userStats, combo.Model) - result.Costs.ByUser[user] += userCost + for user, userStats := range combination.ByUser { + userCost := calculateCost(userStats, combination.Model, combination.ServiceTier) + if aggregateUserCosts != nil { + aggregateUserCosts[user] += userCost + } - comboJSON.ByUser[user] = UsageStatsJSON{ + combinationJSON.ByUser[user] = UsageStatsJSON{ RequestCount: userStats.RequestCount, InputTokens: userStats.InputTokens, OutputTokens: userStats.OutputTokens, @@ -281,12 +822,80 @@ func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { } } - result.Combinations[i] = comboJSON + result[index] = combinationJSON + } + + return result, roundCost(totalCost) +} + +func formatUTCOffsetLabel(timestamp time.Time) string { + _, offsetSeconds := timestamp.Zone() + sign := "+" + if offsetSeconds < 0 { + sign = "-" + offsetSeconds = -offsetSeconds + } + offsetHours := offsetSeconds / 3600 + offsetMinutes := (offsetSeconds % 3600) / 60 + if offsetMinutes == 0 { + return fmt.Sprintf("UTC%s%d", sign, offsetHours) + } + return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes) +} + +func formatWeekStartKey(cycleStartAt time.Time) string { + localCycleStart := cycleStartAt.In(time.Local) + return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart)) +} + +func buildByWeekCost(combinations []CostCombination) map[string]float64 { + byWeek := make(map[string]float64) + for _, combination := range combinations { + if combination.WeekStartUnix <= 0 { + continue + } + weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC() + weekKey := formatWeekStartKey(weekStartAt) + byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier) + } + for weekKey, weekCost := range byWeek { + byWeek[weekKey] = roundCost(weekCost) + } + return byWeek +} + +func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 { + if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() { + return 0 + } + windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute + return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix() +} + +func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON { + u.mutex.Lock() + defer u.mutex.Unlock() + + result := &AggregatedUsageJSON{ + LastUpdated: u.LastUpdated, + Costs: CostsSummaryJSON{ + TotalUSD: 0, + ByUser: make(map[string]float64), + ByWeek: make(map[string]float64), + }, + } + + globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser) + result.Combinations = globalCombinationsJSON + result.Costs.TotalUSD = totalCost + result.Costs.ByWeek = buildByWeekCost(u.Combinations) + + if len(result.Costs.ByWeek) == 0 { + result.Costs.ByWeek = nil } - result.Costs.TotalUSD = math.Round(result.Costs.TotalUSD*100) / 100 for user, cost := range result.Costs.ByUser { - result.Costs.ByUser[user] = math.Round(cost*100) / 100 + result.Costs.ByUser[user] = roundCost(cost) } return result @@ -296,6 +905,9 @@ func (u *AggregatedUsage) Load() error { u.mutex.Lock() defer u.mutex.Unlock() + u.LastUpdated = time.Time{} + u.Combinations = nil + data, err := os.ReadFile(u.filePath) if err != nil { if os.IsNotExist(err) { @@ -316,12 +928,7 @@ func (u *AggregatedUsage) Load() error { u.LastUpdated = temp.LastUpdated u.Combinations = temp.Combinations - - for i := range u.Combinations { - if u.Combinations[i].ByUser == nil { - u.Combinations[i].ByUser = make(map[string]UsageStats) - } - } + normalizeCombinations(u.Combinations) return nil } @@ -349,47 +956,27 @@ func (u *AggregatedUsage) Save() error { return err } -func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, user string) error { +func (u *AggregatedUsage) AddUsage(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error { + return u.AddUsageWithCycleHint(model, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil) +} + +func (u *AggregatedUsage) AddUsageWithCycleHint(model string, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error { if model == "" { return E.New("model cannot be empty") } + normalizedServiceTier := normalizeServiceTier(serviceTier) + if observedAt.IsZero() { + observedAt = time.Now() + } + u.mutex.Lock() defer u.mutex.Unlock() - u.LastUpdated = time.Now() + u.LastUpdated = observedAt + weekStartUnix := deriveWeekStartUnix(cycleHint) - var combo *CostCombination - for i := range u.Combinations { - if u.Combinations[i].Model == model { - combo = &u.Combinations[i] - break - } - } - - if combo == nil { - newCombo := CostCombination{ - Model: model, - Total: UsageStats{}, - ByUser: make(map[string]UsageStats), - } - u.Combinations = append(u.Combinations, newCombo) - combo = &u.Combinations[len(u.Combinations)-1] - } - - combo.Total.RequestCount++ - combo.Total.InputTokens += inputTokens - combo.Total.OutputTokens += outputTokens - combo.Total.CachedTokens += cachedTokens - - if user != "" { - userStats := combo.ByUser[user] - userStats.RequestCount++ - userStats.InputTokens += inputTokens - userStats.OutputTokens += outputTokens - userStats.CachedTokens += cachedTokens - combo.ByUser[user] = userStats - } + addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, weekStartUnix, user, inputTokens, outputTokens, cachedTokens) go u.scheduleSave() From cf4791f1ad90d135b68bbc3957f731690da92793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Feb 2026 12:54:00 +0800 Subject: [PATCH 02/12] platform: Improve iOS OOM killer --- .github/CRONET_GO_VERSION | 2 +- adapter/connections.go | 4 + box.go | 5 +- cmd/internal/build_libbox/main.go | 2 +- common/conntrack/conn.go | 54 ---------- common/conntrack/killer.go | 35 ------- common/conntrack/packet_conn.go | 55 ---------- common/conntrack/track.go | 47 --------- common/conntrack/track_disable.go | 5 - common/conntrack/track_enable.go | 5 - common/dialer/default.go | 24 +++-- constant/proxy.go | 1 + daemon/instance.go | 13 +++ daemon/started_service.go | 15 ++- debug.go | 8 +- experimental/libbox/command_server.go | 1 + experimental/libbox/ffi.json | 4 - experimental/libbox/memory.go | 19 ++-- go.mod | 62 ++++++------ go.sum | 124 +++++++++++------------ include/oom_killer.go | 10 ++ include/registry.go | 1 + option/oom_killer.go | 3 + protocol/naive/outbound.go | 4 + route/conn.go | 124 ++++++++++++++++++----- route/network.go | 7 +- route/route.go | 4 - service/oomkiller/service.go | 138 ++++++++++++++++++++++++++ service/oomkiller/service_stub.go | 39 ++++++++ 29 files changed, 458 insertions(+), 357 deletions(-) delete mode 100644 common/conntrack/conn.go delete mode 100644 common/conntrack/killer.go delete mode 100644 common/conntrack/packet_conn.go delete mode 100644 common/conntrack/track.go delete mode 100644 common/conntrack/track_disable.go delete mode 100644 common/conntrack/track_enable.go create mode 100644 include/oom_killer.go create mode 100644 option/oom_killer.go create mode 100644 service/oomkiller/service.go create mode 100644 service/oomkiller/service_stub.go diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index a703eff5..52565262 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -abd78bb191a815236485ad929716845ffb41465a +34ec1a064c64f274c4e70bf7a9c7de4bb12331f6 diff --git a/adapter/connections.go b/adapter/connections.go index 0682d05a..a0b9c0ef 100644 --- a/adapter/connections.go +++ b/adapter/connections.go @@ -9,6 +9,10 @@ import ( type ConnectionManager interface { Lifecycle + Count() int + CloseAll() + TrackConn(conn net.Conn) net.Conn + TrackPacketConn(conn net.PacketConn) net.PacketConn NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } diff --git a/box.go b/box.go index 7885b0d4..fe116b31 100644 --- a/box.go +++ b/box.go @@ -125,7 +125,10 @@ func New(options Options) (*Box, error) { ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) - applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + err := applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) + if err != nil { + return nil, err + } var needCacheFile bool var needClashAPI bool var needV2RayAPI bool diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index e9cbb1ef..c1282169 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -63,7 +63,7 @@ func init() { sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0") debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -X internal/godebug.defaultGODEBUG=multipathtcp=0 -checklinkname=0") - sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0") + sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_naive_outbound", "with_clash_api", "badlinkname", "tfogo_checklinkname0") darwinTags = append(darwinTags, "with_dhcp", "grpcnotrace") // memcTags = append(memcTags, "with_tailscale") sharedTags = append(sharedTags, "with_tailscale", "ts_omit_logtail", "ts_omit_ssh", "ts_omit_drive", "ts_omit_taildrop", "ts_omit_webclient", "ts_omit_doctor", "ts_omit_capture", "ts_omit_kube", "ts_omit_aws", "ts_omit_synology", "ts_omit_bird") diff --git a/common/conntrack/conn.go b/common/conntrack/conn.go deleted file mode 100644 index 4773d6a8..00000000 --- a/common/conntrack/conn.go +++ /dev/null @@ -1,54 +0,0 @@ -package conntrack - -import ( - "io" - "net" - - "github.com/sagernet/sing/common/x/list" -) - -type Conn struct { - net.Conn - element *list.Element[io.Closer] -} - -func NewConn(conn net.Conn) (net.Conn, error) { - connAccess.Lock() - element := openConnection.PushBack(conn) - connAccess.Unlock() - if KillerEnabled { - err := KillerCheck() - if err != nil { - conn.Close() - return nil, err - } - } - return &Conn{ - Conn: conn, - element: element, - }, nil -} - -func (c *Conn) Close() error { - if c.element.Value != nil { - connAccess.Lock() - if c.element.Value != nil { - openConnection.Remove(c.element) - c.element.Value = nil - } - connAccess.Unlock() - } - return c.Conn.Close() -} - -func (c *Conn) Upstream() any { - return c.Conn -} - -func (c *Conn) ReaderReplaceable() bool { - return true -} - -func (c *Conn) WriterReplaceable() bool { - return true -} diff --git a/common/conntrack/killer.go b/common/conntrack/killer.go deleted file mode 100644 index e0a71e5c..00000000 --- a/common/conntrack/killer.go +++ /dev/null @@ -1,35 +0,0 @@ -package conntrack - -import ( - runtimeDebug "runtime/debug" - "time" - - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/memory" -) - -var ( - KillerEnabled bool - MemoryLimit uint64 - killerLastCheck time.Time -) - -func KillerCheck() error { - if !KillerEnabled { - return nil - } - nowTime := time.Now() - if nowTime.Sub(killerLastCheck) < 3*time.Second { - return nil - } - killerLastCheck = nowTime - if memory.Total() > MemoryLimit { - Close() - go func() { - time.Sleep(time.Second) - runtimeDebug.FreeOSMemory() - }() - return E.New("out of memory") - } - return nil -} diff --git a/common/conntrack/packet_conn.go b/common/conntrack/packet_conn.go deleted file mode 100644 index c7274637..00000000 --- a/common/conntrack/packet_conn.go +++ /dev/null @@ -1,55 +0,0 @@ -package conntrack - -import ( - "io" - "net" - - "github.com/sagernet/sing/common/bufio" - "github.com/sagernet/sing/common/x/list" -) - -type PacketConn struct { - net.PacketConn - element *list.Element[io.Closer] -} - -func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) { - connAccess.Lock() - element := openConnection.PushBack(conn) - connAccess.Unlock() - if KillerEnabled { - err := KillerCheck() - if err != nil { - conn.Close() - return nil, err - } - } - return &PacketConn{ - PacketConn: conn, - element: element, - }, nil -} - -func (c *PacketConn) Close() error { - if c.element.Value != nil { - connAccess.Lock() - if c.element.Value != nil { - openConnection.Remove(c.element) - c.element.Value = nil - } - connAccess.Unlock() - } - return c.PacketConn.Close() -} - -func (c *PacketConn) Upstream() any { - return bufio.NewPacketConn(c.PacketConn) -} - -func (c *PacketConn) ReaderReplaceable() bool { - return true -} - -func (c *PacketConn) WriterReplaceable() bool { - return true -} diff --git a/common/conntrack/track.go b/common/conntrack/track.go deleted file mode 100644 index 2c3e328b..00000000 --- a/common/conntrack/track.go +++ /dev/null @@ -1,47 +0,0 @@ -package conntrack - -import ( - "io" - "sync" - - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/x/list" -) - -var ( - connAccess sync.RWMutex - openConnection list.List[io.Closer] -) - -func Count() int { - if !Enabled { - return 0 - } - return openConnection.Len() -} - -func List() []io.Closer { - if !Enabled { - return nil - } - connAccess.RLock() - defer connAccess.RUnlock() - connList := make([]io.Closer, 0, openConnection.Len()) - for element := openConnection.Front(); element != nil; element = element.Next() { - connList = append(connList, element.Value) - } - return connList -} - -func Close() { - if !Enabled { - return - } - connAccess.Lock() - defer connAccess.Unlock() - for element := openConnection.Front(); element != nil; element = element.Next() { - common.Close(element.Value) - element.Value = nil - } - openConnection.Init() -} diff --git a/common/conntrack/track_disable.go b/common/conntrack/track_disable.go deleted file mode 100644 index 174d8b6e..00000000 --- a/common/conntrack/track_disable.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !with_conntrack - -package conntrack - -const Enabled = false diff --git a/common/conntrack/track_enable.go b/common/conntrack/track_enable.go deleted file mode 100644 index a4bf9986..00000000 --- a/common/conntrack/track_enable.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build with_conntrack - -package conntrack - -const Enabled = true diff --git a/common/dialer/default.go b/common/dialer/default.go index 3ab2e05a..ad37834c 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -9,7 +9,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/listener" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" @@ -37,6 +36,7 @@ type DefaultDialer struct { udpAddr4 string udpAddr6 string netns string + connectionManager adapter.ConnectionManager networkManager adapter.NetworkManager networkStrategy *C.NetworkStrategy defaultNetworkStrategy bool @@ -47,6 +47,7 @@ type DefaultDialer struct { } func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { + connectionManager := service.FromContext[adapter.ConnectionManager](ctx) networkManager := service.FromContext[adapter.NetworkManager](ctx) platformInterface := service.FromContext[adapter.PlatformInterface](ctx) @@ -206,6 +207,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial udpAddr4: udpAddr4, udpAddr6: udpAddr6, netns: options.NetNs, + connectionManager: connectionManager, networkManager: networkManager, networkStrategy: networkStrategy, defaultNetworkStrategy: defaultNetworkStrategy, @@ -238,7 +240,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address return nil, E.New("domain not resolved") } if d.networkStrategy == nil { - return trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) { + return d.trackConn(listener.ListenNetworkNamespace[net.Conn](d.netns, func() (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkUDP: if !address.IsIPv6() { @@ -303,12 +305,12 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin if !fastFallback && !isPrimary { d.networkLastFallback.Store(time.Now()) } - return trackConn(conn, nil) + return d.trackConn(conn, nil) } func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if d.networkStrategy == nil { - return trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) { + return d.trackPacketConn(listener.ListenNetworkNamespace[net.PacketConn](d.netns, func() (net.PacketConn, error) { if destination.IsIPv6() { return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6) } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { @@ -360,23 +362,23 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina return nil, err } } - return trackPacketConn(packetConn, nil) + return d.trackPacketConn(packetConn, nil) } func (d *DefaultDialer) WireGuardControl() control.Func { return d.udpListener.Control } -func trackConn(conn net.Conn, err error) (net.Conn, error) { - if !conntrack.Enabled || err != nil { +func (d *DefaultDialer) trackConn(conn net.Conn, err error) (net.Conn, error) { + if d.connectionManager == nil || err != nil { return conn, err } - return conntrack.NewConn(conn) + return d.connectionManager.TrackConn(conn), nil } -func trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) { - if !conntrack.Enabled || err != nil { +func (d *DefaultDialer) trackPacketConn(conn net.PacketConn, err error) (net.PacketConn, error) { + if d.connectionManager == nil || err != nil { return conn, err } - return conntrack.NewPacketConn(conn) + return d.connectionManager.TrackPacketConn(conn), nil } diff --git a/constant/proxy.go b/constant/proxy.go index a5193623..4130c631 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -30,6 +30,7 @@ const ( TypeSSMAPI = "ssm-api" TypeCCM = "ccm" TypeOCM = "ocm" + TypeOOMKiller = "oom-killer" ) const ( diff --git a/daemon/instance.go b/daemon/instance.go index 5947452f..4ed74182 100644 --- a/daemon/instance.go +++ b/daemon/instance.go @@ -7,10 +7,12 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/service" @@ -21,6 +23,7 @@ type Instance struct { ctx context.Context cancel context.CancelFunc instance *box.Box + connectionManager adapter.ConnectionManager clashServer adapter.ClashServer cacheFile adapter.CacheFile pauseManager pause.Manager @@ -84,6 +87,15 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove } } } + if s.oomKiller && C.IsIos { + if !common.Any(options.Services, func(it option.Service) bool { + return it.Type == C.TypeOOMKiller + }) { + options.Services = append(options.Services, option.Service{ + Type: C.TypeOOMKiller, + }) + } + } urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) i := &Instance{ @@ -101,6 +113,7 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove return nil, err } i.instance = boxInstance + i.connectionManager = service.FromContext[adapter.ConnectionManager](ctx) i.clashServer = service.FromContext[adapter.ClashServer](ctx) i.pauseManager = service.FromContext[pause.Manager](ctx) i.cacheFile = service.FromContext[adapter.CacheFile](ctx) diff --git a/daemon/started_service.go b/daemon/started_service.go index 7c6fa945..5e677f7a 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -8,7 +8,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/experimental/clashapi" "github.com/sagernet/sing-box/experimental/clashapi/trafficontrol" @@ -36,6 +35,7 @@ type StartedService struct { handler PlatformHandler debug bool logMaxLines int + oomKiller bool // workingDirectory string // tempDirectory string // userID int @@ -67,6 +67,7 @@ type ServiceOptions struct { Handler PlatformHandler Debug bool LogMaxLines int + OOMKiller bool // WorkingDirectory string // TempDirectory string // UserID int @@ -81,6 +82,7 @@ func NewStartedService(options ServiceOptions) *StartedService { handler: options.Handler, debug: options.Debug, logMaxLines: options.LogMaxLines, + oomKiller: options.OOMKiller, // workingDirectory: options.WorkingDirectory, // tempDirectory: options.TempDirectory, // userID: options.UserID, @@ -409,10 +411,12 @@ func (s *StartedService) readStatus() *Status { var status Status status.Memory = memory.Inuse() status.Goroutines = int32(runtime.NumGoroutine()) - status.ConnectionsOut = int32(conntrack.Count()) s.serviceAccess.RLock() nowService := s.instance s.serviceAccess.RUnlock() + if nowService != nil && nowService.connectionManager != nil { + status.ConnectionsOut = int32(nowService.connectionManager.Count()) + } if nowService != nil { if clashServer := nowService.clashServer; clashServer != nil { status.TrafficAvailable = true @@ -993,7 +997,12 @@ func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConn } func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { - conntrack.Close() + s.serviceAccess.RLock() + nowService := s.instance + s.serviceAccess.RUnlock() + if nowService != nil && nowService.connectionManager != nil { + nowService.connectionManager.CloseAll() + } return &emptypb.Empty{}, nil } diff --git a/debug.go b/debug.go index 1726c10e..f620172b 100644 --- a/debug.go +++ b/debug.go @@ -3,11 +3,11 @@ package box import ( "runtime/debug" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" ) -func applyDebugOptions(options option.DebugOptions) { +func applyDebugOptions(options option.DebugOptions) error { applyDebugListenOption(options) if options.GCPercent != nil { debug.SetGCPercent(*options.GCPercent) @@ -26,9 +26,9 @@ func applyDebugOptions(options option.DebugOptions) { } if options.MemoryLimit.Value() != 0 { debug.SetMemoryLimit(int64(float64(options.MemoryLimit.Value()) / 1.5)) - conntrack.MemoryLimit = options.MemoryLimit.Value() } if options.OOMKiller != nil { - conntrack.KillerEnabled = *options.OOMKiller + return E.New("legacy oom_killer in debug options is removed, use oom-killer service instead") } + return nil } diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index e3300281..1c2412b6 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -60,6 +60,7 @@ func NewCommandServer(handler CommandServerHandler, platformInterface PlatformIn Handler: (*platformHandler)(server), Debug: sDebug, LogMaxLines: sLogMaxLines, + OOMKiller: memoryLimitEnabled, // WorkingDirectory: sWorkingPath, // TempDirectory: sTempPath, // UserID: sUserID, diff --git a/experimental/libbox/ffi.json b/experimental/libbox/ffi.json index 28333871..81fae27d 100644 --- a/experimental/libbox/ffi.json +++ b/experimental/libbox/ffi.json @@ -29,7 +29,6 @@ "with_utls", "with_naive_outbound", "with_clash_api", - "with_conntrack", "badlinkname", "tfogo_checklinkname0", "with_tailscale", @@ -59,7 +58,6 @@ "with_wireguard", "with_utls", "with_clash_api", - "with_conntrack", "badlinkname", "tfogo_checklinkname0", "with_tailscale", @@ -90,7 +88,6 @@ "with_utls", "with_naive_outbound", "with_clash_api", - "with_conntrack", "badlinkname", "tfogo_checklinkname0", "with_dhcp", @@ -134,7 +131,6 @@ "with_naive_outbound", "with_purego", "with_clash_api", - "with_conntrack", "badlinkname", "tfogo_checklinkname0", "with_tailscale", diff --git a/experimental/libbox/memory.go b/experimental/libbox/memory.go index b10c6701..b0b87f73 100644 --- a/experimental/libbox/memory.go +++ b/experimental/libbox/memory.go @@ -4,20 +4,23 @@ import ( "math" runtimeDebug "runtime/debug" - "github.com/sagernet/sing-box/common/conntrack" + C "github.com/sagernet/sing-box/constant" ) +var memoryLimitEnabled bool + func SetMemoryLimit(enabled bool) { - const memoryLimit = 45 * 1024 * 1024 - const memoryLimitGo = memoryLimit / 1.5 + memoryLimitEnabled = enabled + const memoryLimitGo = 45 * 1024 * 1024 if enabled { runtimeDebug.SetGCPercent(10) - runtimeDebug.SetMemoryLimit(memoryLimitGo) - conntrack.KillerEnabled = true - conntrack.MemoryLimit = memoryLimit + if C.IsIos { + runtimeDebug.SetMemoryLimit(memoryLimitGo) + } } else { runtimeDebug.SetGCPercent(100) - runtimeDebug.SetMemoryLimit(math.MaxInt64) - conntrack.KillerEnabled = false + if C.IsIos { + runtimeDebug.SetMemoryLimit(math.MaxInt64) + } } } diff --git a/go.mod b/go.mod index 0523b73f..506b54cf 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260221042137-abd78bb191a8 - github.com/sagernet/cronet-go/all v0.0.0-20260221042137-abd78bb191a8 + github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64 + github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.11 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -105,35 +105,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260221041448-e52d68fd87fe // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260221041448-e52d68fd87fe // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 738a415b..c9478c27 100644 --- a/go.sum +++ b/go.sum @@ -152,68 +152,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260221042137-abd78bb191a8 h1:XcZiLUXnYE74RvqVdsyxgIInBuFaZbABx2Hom5U6uuk= -github.com/sagernet/cronet-go v0.0.0-20260221042137-abd78bb191a8/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260221042137-abd78bb191a8 h1:uaUy9opPmPYD+viUeUnBzT+lw5b19j6pC/iKew7u13I= -github.com/sagernet/cronet-go/all v0.0.0-20260221042137-abd78bb191a8/go.mod h1:Gn1d0D8adjp7mlgSv+/pVLJsG+engIMBp/R4+1MOhlk= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260221041448-e52d68fd87fe h1:iKIZJsvD+D3sdAzAeeOodJBxnFL9OVs1LTq3xnmQ6wQ= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260221041448-e52d68fd87fe h1:/YhWKKVb3uQ5JmBQwFEOKg8QK2w0Ky6dxEb/UHrhQww= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260221041448-e52d68fd87fe h1:h+XF746wRtYKavUeS8//Vro6s9f0F6+pI8VQFLMLg6E= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:yMs96D9ErwAG8gEHV6zaQ5cp9ZPNBHExxJ5+u8cZ644= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260221041448-e52d68fd87fe h1:HUJtGjXcB+70W+YfeLgue6X1u69XLN0Ar56Ipg3gtvY= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:ivo7JwVqDTMf/qVfpKYdwcIc+NzKGyMJ/WLj/TTNYXg= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe h1:HdWJLwa/Ie3jsueJ0O2mZd4V/NP1UJ6bamdcNHWsYEo= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:A9PWi2xCI+TCr9ALr+BO76WCCk1JnRyjeEH0/+rdyRc= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe h1:aMOUWbGjkPBFqObA+uAJOfVuBkHfvz2sibNgOJqjuBs= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260221041448-e52d68fd87fe h1:CzE+sJ2iOvJwOuZhpiDV5VlQrBaNJAZhDCafly+TH9c= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260221041448-e52d68fd87fe h1:2grC2CeyUiYVgqG7BGKpJvjFzYv0wL64QMoBqOHVZsI= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260221041448-e52d68fd87fe h1:+N9/LauocInR5kxXU+L5bQe1bndCZUC+6L0FaozWZNI= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260221041448-e52d68fd87fe h1:mRJcjGtKG/eaPL4sZ4Ij+e7aLdg1AEXNI1PgRnxI6H8= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260221041448-e52d68fd87fe h1:UkWiTAxUAjTtsu7e52cvMrmbShz+ahTdGkhF9mEIIZU= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:giJVex0bwZy+DwmPwfZ+NZmafBRTsaZ+QUaD2Fkacxs= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260221041448-e52d68fd87fe h1:XkjAQkciY78eSMF/9VdaWRWb+OfPvoIxVKx5gHGfSIg= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260221041448-e52d68fd87fe h1:8uDfbPXAL0MWqGI8bm6YJghRmGvK08z4jEIGoODKqTI= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260221041448-e52d68fd87fe h1:Ucs4htbATTdG7YGHCyQ4vMYRhltVvsapZ95THRNssr4= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260221041448-e52d68fd87fe h1:oeQjTH4lveV4M7/hqOJFfwQ9UfWvkFZEXTc00R2acuk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260221041448-e52d68fd87fe h1:NWABhpSuXcN61hF0CUqwliJXxEbmHidoAHxtB61V3GA= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260221041448-e52d68fd87fe h1:+0VrQdlGR/zLjPzinXFqFR2sdzF2BXoXu7f8xaMuwtg= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260221041448-e52d68fd87fe h1:DYW55QJOZBI4Znjhc0IiusF+IMg4R2dHPX0KnZC6gSo= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260221041448-e52d68fd87fe h1:xbbZtyXOxYJMplsyv371ddQb7QrEnyXIIGdUK/3WNTE= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260221041448-e52d68fd87fe h1:YSH2lVT+Sn29lQQbwhDpxZvGjVSg80SUfW4JQ8vM3aA= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe h1:gQ1veofYJr8Z1hBVM2PIrn4+EMKvwh+zWpYBr+mxgQ8= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:e2TMlbEottRCDfTWxUSw4Jl5dK8IInV02XIvLKVjLbM= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe h1:Cgh+DP/Ns1djisz+LFxA1nEhyF6EEU5ZdVxNTkiX2BI= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260221041448-e52d68fd87fe h1:VCtjRmkI1IkKdWQ3Jh7j/ze5fhBQJZo1JR70cVKLaKw= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260221041448-e52d68fd87fe h1:SKePXZMEPUY5zA1VFBPbPOxZsfb/wkMNZAvjPO7hL+I= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260221041448-e52d68fd87fe/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64 h1:2hcUvxlEbzs4hbt9l0oeBpUfet2PHzaV6X6zNTDMkvQ= +github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64 h1:f5TU9VAbhtm/SMLKctjMiSNUoQso4S6kEPUm5yNglNo= +github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64/go.mod h1:ohbVnfJvBzdv7R87Nz0fAmkLSTF9lCKrQC2rIqsQPY4= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717 h1:vtE5zVWFQuNls2jHb5edFCFYJblGY9qrqTVpoW2V/h4= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717 h1:+d5efMdtHbizXb4Vuai/11uNpWXmwOCfVgQcICA3kgk= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717 h1:6BYObS6w5sWnNLChKT1fOYJnJt/+p6du8fSyDb3DLwo= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717 h1:UbgjfMai+duPsftAT94zr4KcnACuJhjaxQkpup6TWLk= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717 h1:KRrzFzsWWgy8XPHfp2hEX7wTuc7ILQZiFKIATvuyCHU= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717 h1:bTp8j/GqUsS0P2aaTcbs7alSvUoywx+YIWyoCcVA05I= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717 h1:PKuCYfGpSfnzgRJjRR5em/yU5SoI4Yvx3r0C2k1Ksh4= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717 h1:w5ytvTm42ncUIFbLueN/ilp2ZaHrr5GC76Kc/Oxw4qI= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717 h1:2BjQH02cCF6spz/WG8bJHBPhZtF4Or5D73L+n5fZbNE= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717 h1:N9KUwDODj5eP5uqdSmbMufml2nGPc7oX4H5NPAjADIs= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717 h1:v2p6tqpQVI6RR1HamuJmkz01Ght4miQ79YhzQtOYowk= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717 h1:glPUIWUUAlOII+L0UI7/JAJ8o0wqAT7qVmWB4AcPyWs= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717 h1:0QKgSc5nBrR6H1ImsOlaBbVQ7onoFhwxTBwiOnUn0es= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717 h1:DsrfMoWf05lLrwdo0s/6wAC/4smeDgT4+t/iJtwGD+g= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717 h1:bYqI37NWgvxVBeWL50/ts23uGiyHv7QfH/Ylu5lvgLs= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717 h1:zg8keyQA2sniD4gqMwbUVdf57M1cmGobumOtZy1CYJs= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717 h1:cgyVkTyccMGiCayGgcvfc+q0b5GdhqSk+3UMmGhlCi0= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717 h1:anuE3B73oIvDOGJGvenfkQTm3NY2vL84aM062t8DZ4Y= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717 h1:kYN/amWzLOZMm5evPwlmumnswBNPeFZiKINKP73AsuA= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717 h1:S7rlC1Knzs3+It2u6AnhGUgiLU1QUzvoV3JXWvwE4n8= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717 h1:FeyB5ZyFHAWkU1DETmUjLEM9GUFWoIndV8usT4AjGOk= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717 h1:loU7PsyqI7dplbFGjQKoMT4IVYuzVyfW88FIq+7gbmY= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717 h1:XSVrZ18XauiYMf+G0CzZETteQF0r1NOoA1gVXlZWjUI= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717 h1:oTRK4T5pQ1ZYQh7Avr5u3bsDnXkloSVqwBljRy8FHOY= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717 h1:J18xRmUa9lle+Y6pYkQ7OMq2B9eox5Bmm4MkbuQPQm0= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717 h1:PC9gKeDN3wmcFxwiBEM57W0YwEz1uFGqawwssEQe494= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717 h1:STlbMnsydaymby26m3wNfeYYI7HoiZ6+wxQ75Si1g8E= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717 h1:qWtpoBxmHyb49IRtMAFkJIRlL4KCYW5Fe6YptYhItpM= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717 h1:7ohuWJ0PswrU7Xw/xfL6OrIh7sXybGNOgh6DaL+urdY= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.11 h1:niMQAspvuThup5eRZQpsGcbM76zAvnsGr7RUIpnQMDQ= diff --git a/include/oom_killer.go b/include/oom_killer.go new file mode 100644 index 00000000..3f70d9d0 --- /dev/null +++ b/include/oom_killer.go @@ -0,0 +1,10 @@ +package include + +import ( + "github.com/sagernet/sing-box/adapter/service" + "github.com/sagernet/sing-box/service/oomkiller" +) + +func registerOOMKillerService(registry *service.Registry) { + oomkiller.RegisterService(registry) +} diff --git a/include/registry.go b/include/registry.go index d909b850..64a49b61 100644 --- a/include/registry.go +++ b/include/registry.go @@ -137,6 +137,7 @@ func ServiceRegistry() *service.Registry { registerDERPService(registry) registerCCMService(registry) registerOCMService(registry) + registerOOMKillerService(registry) return registry } diff --git a/option/oom_killer.go b/option/oom_killer.go new file mode 100644 index 00000000..9fbbde84 --- /dev/null +++ b/option/oom_killer.go @@ -0,0 +1,3 @@ +package option + +type OOMKillerServiceOptions struct{} diff --git a/protocol/naive/outbound.go b/protocol/naive/outbound.go index dcc1aec5..8249a1fe 100644 --- a/protocol/naive/outbound.go +++ b/protocol/naive/outbound.go @@ -254,6 +254,10 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return h.uotClient.ListenPacket(ctx, destination) } +func (h *Outbound) InterfaceUpdated() { + h.client.Engine().CloseAllConnections() +} + func (h *Outbound) Close() error { return h.client.Close() } diff --git a/route/conn.go b/route/conn.go index 3e9c831c..899d2939 100644 --- a/route/conn.go +++ b/route/conn.go @@ -44,16 +44,52 @@ func (m *ConnectionManager) Start(stage adapter.StartStage) error { return nil } -func (m *ConnectionManager) Close() error { +func (m *ConnectionManager) Count() int { + return m.connections.Len() +} + +func (m *ConnectionManager) CloseAll() { m.access.Lock() - defer m.access.Unlock() - for element := m.connections.Front(); element != nil; element = element.Next() { - common.Close(element.Value) + var closers []io.Closer + for element := m.connections.Front(); element != nil; { + nextElement := element.Next() + closers = append(closers, element.Value) + m.connections.Remove(element) + element = nextElement } - m.connections.Init() + m.access.Unlock() + for _, closer := range closers { + common.Close(closer) + } +} + +func (m *ConnectionManager) Close() error { + m.CloseAll() return nil } +func (m *ConnectionManager) TrackConn(conn net.Conn) net.Conn { + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + return &trackedConn{ + Conn: conn, + manager: m, + element: element, + } +} + +func (m *ConnectionManager) TrackPacketConn(conn net.PacketConn) net.PacketConn { + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + return &trackedPacketConn{ + PacketConn: conn, + manager: m, + element: element, + } +} + func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = adapter.WithContext(ctx, &metadata) var ( @@ -92,14 +128,6 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co if metadata.TLSFragment || metadata.TLSRecordFragment { remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay) } - m.access.Lock() - element := m.connections.PushBack(conn) - m.access.Unlock() - onClose = N.AppendClose(onClose, func(it error) { - m.access.Lock() - defer m.access.Unlock() - m.connections.Remove(element) - }) var done atomic.Bool if m.kickWriteHandshake(ctx, conn, remoteConn, false, &done, onClose) { return @@ -216,14 +244,6 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout) } destination := bufio.NewPacketConn(remotePacketConn) - m.access.Lock() - element := m.connections.PushBack(conn) - m.access.Unlock() - onClose = N.AppendClose(onClose, func(it error) { - m.access.Lock() - defer m.access.Unlock() - m.connections.Remove(element) - }) var done atomic.Bool go m.packetConnectionCopy(ctx, conn, destination, false, &done, onClose) go m.packetConnectionCopy(ctx, destination, conn, true, &done, onClose) @@ -242,7 +262,9 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn, destination.Close() } if done.Swap(true) { - onClose(err) + if onClose != nil { + onClose(err) + } common.Close(source, destination) } if !direction { @@ -303,7 +325,9 @@ func (m *ConnectionManager) kickWriteHandshake(ctx context.Context, source net.C return false } if !done.Swap(true) { - onClose(err) + if onClose != nil { + onClose(err) + } } common.Close(source, destination) if !direction { @@ -334,7 +358,59 @@ func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.P } } if !done.Swap(true) { - onClose(err) + if onClose != nil { + onClose(err) + } } common.Close(source, destination) } + +type trackedConn struct { + net.Conn + manager *ConnectionManager + element *list.Element[io.Closer] +} + +func (c *trackedConn) Close() error { + c.manager.access.Lock() + c.manager.connections.Remove(c.element) + c.manager.access.Unlock() + return c.Conn.Close() +} + +func (c *trackedConn) Upstream() any { + return c.Conn +} + +func (c *trackedConn) ReaderReplaceable() bool { + return true +} + +func (c *trackedConn) WriterReplaceable() bool { + return true +} + +type trackedPacketConn struct { + net.PacketConn + manager *ConnectionManager + element *list.Element[io.Closer] +} + +func (c *trackedPacketConn) Close() error { + c.manager.access.Lock() + c.manager.connections.Remove(c.element) + c.manager.access.Unlock() + return c.PacketConn.Close() +} + +func (c *trackedPacketConn) Upstream() any { + return bufio.NewPacketConn(c.PacketConn) +} + +func (c *trackedPacketConn) ReaderReplaceable() bool { + return true +} + +func (c *trackedPacketConn) WriterReplaceable() bool { + return true +} diff --git a/route/network.go b/route/network.go index b53142b5..b8eefdc0 100644 --- a/route/network.go +++ b/route/network.go @@ -13,7 +13,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/settings" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" @@ -48,6 +47,7 @@ type NetworkManager struct { powerListener winpowrprof.EventListener pauseManager pause.Manager platformInterface adapter.PlatformInterface + connectionManager adapter.ConnectionManager endpoint adapter.EndpointManager inbound adapter.InboundManager outbound adapter.OutboundManager @@ -90,6 +90,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options }, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[adapter.PlatformInterface](ctx), + connectionManager: service.FromContext[adapter.ConnectionManager](ctx), endpoint: service.FromContext[adapter.EndpointManager](ctx), inbound: service.FromContext[adapter.InboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), @@ -450,7 +451,9 @@ func (r *NetworkManager) UpdateWIFIState() { } func (r *NetworkManager) ResetNetwork() { - conntrack.Close() + if r.connectionManager != nil { + r.connectionManager.CloseAll() + } for _, endpoint := range r.endpoint.Endpoints() { listener, isListener := endpoint.(adapter.InterfaceUpdateListener) diff --git a/route/route.go b/route/route.go index fd025a1b..240d0343 100644 --- a/route/route.go +++ b/route/route.go @@ -9,7 +9,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" @@ -80,7 +79,6 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad injectable.NewConnectionEx(ctx, conn, metadata, onClose) return nil } - conntrack.KillerCheck() metadata.Network = N.NetworkTCP switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: @@ -216,8 +214,6 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m injectable.NewPacketConnectionEx(ctx, conn, metadata, onClose) return nil } - conntrack.KillerCheck() - // TODO: move to UoT metadata.Network = N.NetworkUDP diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go new file mode 100644 index 00000000..fb486ab9 --- /dev/null +++ b/service/oomkiller/service.go @@ -0,0 +1,138 @@ +//go:build darwin && cgo + +package oomkiller + +/* +#include + +static dispatch_source_t memoryPressureSource; + +extern void goMemoryPressureCallback(unsigned long status); + +static void startMemoryPressureMonitor() { + memoryPressureSource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, + 0, + DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL, + dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) + ); + dispatch_source_set_event_handler(memoryPressureSource, ^{ + unsigned long status = dispatch_source_get_data(memoryPressureSource); + goMemoryPressureCallback(status); + }); + dispatch_activate(memoryPressureSource); +} + +static void stopMemoryPressureMonitor() { + if (memoryPressureSource) { + dispatch_source_cancel(memoryPressureSource); + memoryPressureSource = NULL; + } +} +*/ +import "C" + +import ( + "context" + runtimeDebug "runtime/debug" + "sync" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + boxConstant "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/memory" + "github.com/sagernet/sing/service" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) +} + +var ( + globalAccess sync.Mutex + globalServices []*Service +) + +type Service struct { + boxService.Adapter + logger log.ContextLogger + router adapter.Router +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { + return &Service{ + Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), + logger: logger, + router: service.FromContext[adapter.Router](ctx), + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + globalAccess.Lock() + isFirst := len(globalServices) == 0 + globalServices = append(globalServices, s) + globalAccess.Unlock() + if isFirst { + C.startMemoryPressureMonitor() + } + s.logger.Info("started memory pressure monitor") + return nil +} + +func (s *Service) Close() error { + globalAccess.Lock() + for i, service := range globalServices { + if service == s { + globalServices = append(globalServices[:i], globalServices[i+1:]...) + break + } + } + isLast := len(globalServices) == 0 + globalAccess.Unlock() + if isLast { + C.stopMemoryPressureMonitor() + } + return nil +} + +//export goMemoryPressureCallback +func goMemoryPressureCallback(status C.ulong) { + globalAccess.Lock() + services := make([]*Service, len(globalServices)) + copy(services, globalServices) + globalAccess.Unlock() + if len(services) == 0 { + return + } + criticalFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_CRITICAL) + warnFlag := C.ulong(C.DISPATCH_MEMORYPRESSURE_WARN) + isCritical := status&criticalFlag != 0 + isWarning := status&warnFlag != 0 + var level string + switch { + case isCritical: + level = "critical" + case isWarning: + level = "warning" + default: + level = "normal" + } + for _, s := range services { + if isCritical { + s.logger.Error("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB, resetting network") + s.router.ResetNetwork() + } else if isWarning { + s.logger.Warn("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB") + } else { + s.logger.Debug("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB") + } + } + if isCritical { + runtimeDebug.FreeOSMemory() + } +} diff --git a/service/oomkiller/service_stub.go b/service/oomkiller/service_stub.go new file mode 100644 index 00000000..425f525e --- /dev/null +++ b/service/oomkiller/service_stub.go @@ -0,0 +1,39 @@ +//go:build !darwin || !cgo + +package oomkiller + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + boxService "github.com/sagernet/sing-box/adapter/service" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func RegisterService(registry *boxService.Registry) { + boxService.Register[option.OOMKillerServiceOptions](registry, C.TypeOOMKiller, NewService) +} + +type Service struct { + boxService.Adapter +} + +func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { + return &Service{ + Adapter: boxService.NewAdapter(C.TypeOOMKiller, tag), + }, nil +} + +func (s *Service) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } + return E.New("memory pressure monitoring is not available on this platform") +} + +func (s *Service) Close() error { + return nil +} From 21a1512e6c6faa141ad9a043671562e54375fda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Feb 2026 21:45:11 +0800 Subject: [PATCH 03/12] tailscale: Fix AdvertiseTags --- protocol/tailscale/endpoint.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/protocol/tailscale/endpoint.go b/protocol/tailscale/endpoint.go index 40bc4bc6..659277d9 100644 --- a/protocol/tailscale/endpoint.go +++ b/protocol/tailscale/endpoint.go @@ -210,10 +210,11 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL UserLogf: func(format string, args ...any) { logger.Debug(fmt.Sprintf(format, args...)) }, - Ephemeral: options.Ephemeral, - AuthKey: options.AuthKey, - ControlURL: options.ControlURL, - Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger}, + Ephemeral: options.Ephemeral, + AuthKey: options.AuthKey, + ControlURL: options.ControlURL, + AdvertiseTags: options.AdvertiseTags, + Dialer: &endpointDialer{Dialer: outboundDialer, logger: logger}, LookupHook: func(ctx context.Context, host string) ([]netip.Addr, error) { return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions()) }, @@ -363,12 +364,10 @@ func (t *Endpoint) Start(stage adapter.StartStage) error { Prefs: ipn.Prefs{ RouteAll: t.acceptRoutes, AdvertiseRoutes: t.advertiseRoutes, - AdvertiseTags: t.advertiseTags, }, RouteAllSet: true, ExitNodeIPSet: true, AdvertiseRoutesSet: true, - AdvertiseTagsSet: true, RelayServerPortSet: true, RelayServerStaticEndpointsSet: true, } From 65150f5cc3d5ee593e194a8536c952bb1d47642a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 13:35:58 +0800 Subject: [PATCH 04/12] platform: Improve OOM killer for iOS --- daemon/started_service.go | 2 +- go.mod | 2 +- go.sum | 4 +- option/oom_killer.go | 13 ++- service/oomkiller/config.go | 51 ++++++++++ service/oomkiller/service.go | 82 ++++++++++++--- service/oomkiller/service_stub.go | 54 ++++++++-- service/oomkiller/service_timer.go | 158 +++++++++++++++++++++++++++++ 8 files changed, 341 insertions(+), 25 deletions(-) create mode 100644 service/oomkiller/config.go create mode 100644 service/oomkiller/service_timer.go diff --git a/daemon/started_service.go b/daemon/started_service.go index 5e677f7a..7ebdac1e 100644 --- a/daemon/started_service.go +++ b/daemon/started_service.go @@ -409,7 +409,7 @@ func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server func (s *StartedService) readStatus() *Status { var status Status - status.Memory = memory.Inuse() + status.Memory = memory.Total() status.Goroutines = int32(runtime.NumGoroutine()) s.serviceAccess.RLock() nowService := s.instance diff --git a/go.mod b/go.mod index 506b54cf..37724e7e 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/sagernet/gomobile v0.1.11 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 - github.com/sagernet/sing v0.8.0-beta.16 + github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07 github.com/sagernet/sing-mux v0.3.4 github.com/sagernet/sing-quic v0.6.0-beta.13 github.com/sagernet/sing-shadowsocks v0.2.8 diff --git a/go.sum b/go.sum index c9478c27..8c683a95 100644 --- a/go.sum +++ b/go.sum @@ -226,8 +226,8 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o= github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4= -github.com/sagernet/sing v0.8.0-beta.16 h1:Fe+6E9VHYky9Mx4cf0ugbZPWDcXRflpAu7JQ5bWXvaA= -github.com/sagernet/sing v0.8.0-beta.16/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07 h1:LQqb+xtR5uqF6bePmJQ3sAToF/kMCjxSnz17HnboXA8= +github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s= github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk= github.com/sagernet/sing-quic v0.6.0-beta.13 h1:umDr6GC5fVbOIoTvqV4544wY61zEN+ObQwVGNP8sX1M= diff --git a/option/oom_killer.go b/option/oom_killer.go index 9fbbde84..2032ed09 100644 --- a/option/oom_killer.go +++ b/option/oom_killer.go @@ -1,3 +1,14 @@ package option -type OOMKillerServiceOptions struct{} +import ( + "github.com/sagernet/sing/common/byteformats" + "github.com/sagernet/sing/common/json/badoption" +) + +type OOMKillerServiceOptions struct { + MemoryLimit *byteformats.MemoryBytes `json:"memory_limit,omitempty"` + SafetyMargin *byteformats.MemoryBytes `json:"safety_margin,omitempty"` + MinInterval badoption.Duration `json:"min_interval,omitempty"` + MaxInterval badoption.Duration `json:"max_interval,omitempty"` + ChecksBeforeLimit int `json:"checks_before_limit,omitempty"` +} diff --git a/service/oomkiller/config.go b/service/oomkiller/config.go new file mode 100644 index 00000000..693ced99 --- /dev/null +++ b/service/oomkiller/config.go @@ -0,0 +1,51 @@ +package oomkiller + +import ( + "time" + + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64, useAvailable bool) (timerConfig, error) { + safetyMargin := uint64(defaultSafetyMargin) + if options.SafetyMargin != nil && options.SafetyMargin.Value() > 0 { + safetyMargin = options.SafetyMargin.Value() + } + + minInterval := defaultMinInterval + if options.MinInterval != 0 { + minInterval = time.Duration(options.MinInterval.Build()) + if minInterval <= 0 { + return timerConfig{}, E.New("min_interval must be greater than 0") + } + } + + maxInterval := defaultMaxInterval + if options.MaxInterval != 0 { + maxInterval = time.Duration(options.MaxInterval.Build()) + if maxInterval <= 0 { + return timerConfig{}, E.New("max_interval must be greater than 0") + } + } + if maxInterval < minInterval { + return timerConfig{}, E.New("max_interval must be greater than or equal to min_interval") + } + + checksBeforeLimit := defaultChecksBeforeLimit + if options.ChecksBeforeLimit != 0 { + checksBeforeLimit = options.ChecksBeforeLimit + if checksBeforeLimit <= 0 { + return timerConfig{}, E.New("checks_before_limit must be greater than 0") + } + } + + return timerConfig{ + memoryLimit: memoryLimit, + safetyMargin: safetyMargin, + minInterval: minInterval, + maxInterval: maxInterval, + checksBeforeLimit: checksBeforeLimit, + useAvailable: useAvailable, + }, nil +} diff --git a/service/oomkiller/service.go b/service/oomkiller/service.go index fb486ab9..c3612d92 100644 --- a/service/oomkiller/service.go +++ b/service/oomkiller/service.go @@ -57,37 +57,72 @@ var ( type Service struct { boxService.Adapter - logger log.ContextLogger - router adapter.Router + logger log.ContextLogger + router adapter.Router + memoryLimit uint64 + hasTimerMode bool + useAvailable bool + timerConfig timerConfig + adaptiveTimer *adaptiveTimer } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { - return &Service{ + s := &Service{ Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), logger: logger, router: service.FromContext[adapter.Router](ctx), - }, nil + } + + if options.MemoryLimit != nil { + s.memoryLimit = options.MemoryLimit.Value() + if s.memoryLimit > 0 { + s.hasTimerMode = true + } + } + + config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) + if err != nil { + return nil, err + } + s.timerConfig = config + + return s, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } + + if s.hasTimerMode { + s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) + if s.memoryLimit > 0 { + s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") + } else { + s.logger.Info("started memory monitor with available memory detection") + } + } else { + s.logger.Info("started memory pressure monitor") + } + globalAccess.Lock() isFirst := len(globalServices) == 0 globalServices = append(globalServices, s) globalAccess.Unlock() + if isFirst { C.startMemoryPressureMonitor() } - s.logger.Info("started memory pressure monitor") return nil } func (s *Service) Close() error { + if s.adaptiveTimer != nil { + s.adaptiveTimer.stop() + } globalAccess.Lock() - for i, service := range globalServices { - if service == s { + for i, svc := range globalServices { + if svc == s { globalServices = append(globalServices[:i], globalServices[i+1:]...) break } @@ -122,17 +157,36 @@ func goMemoryPressureCallback(status C.ulong) { default: level = "normal" } + var freeOSMemory bool for _, s := range services { - if isCritical { - s.logger.Error("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB, resetting network") - s.router.ResetNetwork() - } else if isWarning { - s.logger.Warn("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB") + usage := memory.Total() + if s.hasTimerMode { + if isCritical { + s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") + if s.adaptiveTimer != nil { + s.adaptiveTimer.startNow() + } + } else if isWarning { + s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") + } else { + s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") + if s.adaptiveTimer != nil { + s.adaptiveTimer.stop() + } + } } else { - s.logger.Debug("memory pressure: ", level, ", usage: ", memory.Total()/(1024*1024), " MiB") + if isCritical { + s.logger.Error("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB, resetting network") + s.router.ResetNetwork() + freeOSMemory = true + } else if isWarning { + s.logger.Warn("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") + } else { + s.logger.Debug("memory pressure: ", level, ", usage: ", usage/(1024*1024), " MiB") + } } } - if isCritical { + if freeOSMemory { runtimeDebug.FreeOSMemory() } } diff --git a/service/oomkiller/service_stub.go b/service/oomkiller/service_stub.go index 425f525e..13348bac 100644 --- a/service/oomkiller/service_stub.go +++ b/service/oomkiller/service_stub.go @@ -7,33 +7,75 @@ import ( "github.com/sagernet/sing-box/adapter" boxService "github.com/sagernet/sing-box/adapter/service" - C "github.com/sagernet/sing-box/constant" + boxConstant "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/memory" + "github.com/sagernet/sing/service" ) func RegisterService(registry *boxService.Registry) { - boxService.Register[option.OOMKillerServiceOptions](registry, C.TypeOOMKiller, NewService) + boxService.Register[option.OOMKillerServiceOptions](registry, boxConstant.TypeOOMKiller, NewService) } type Service struct { boxService.Adapter + logger log.ContextLogger + router adapter.Router + adaptiveTimer *adaptiveTimer + timerConfig timerConfig + hasTimerMode bool + useAvailable bool + memoryLimit uint64 } func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) { - return &Service{ - Adapter: boxService.NewAdapter(C.TypeOOMKiller, tag), - }, nil + s := &Service{ + Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag), + logger: logger, + router: service.FromContext[adapter.Router](ctx), + } + + if options.MemoryLimit != nil { + s.memoryLimit = options.MemoryLimit.Value() + } + if s.memoryLimit > 0 { + s.hasTimerMode = true + } else if memory.AvailableSupported() { + s.useAvailable = true + s.hasTimerMode = true + } + + config, err := buildTimerConfig(options, s.memoryLimit, s.useAvailable) + if err != nil { + return nil, err + } + s.timerConfig = config + + return s, nil } func (s *Service) Start(stage adapter.StartStage) error { if stage != adapter.StartStateStart { return nil } - return E.New("memory pressure monitoring is not available on this platform") + if !s.hasTimerMode { + return E.New("memory pressure monitoring is not available on this platform without memory_limit") + } + s.adaptiveTimer = newAdaptiveTimer(s.logger, s.router, s.timerConfig) + s.adaptiveTimer.start(0) + if s.useAvailable { + s.logger.Info("started memory monitor with available memory detection") + } else { + s.logger.Info("started memory monitor with limit: ", s.memoryLimit/(1024*1024), " MiB") + } + return nil } func (s *Service) Close() error { + if s.adaptiveTimer != nil { + s.adaptiveTimer.stop() + } return nil } diff --git a/service/oomkiller/service_timer.go b/service/oomkiller/service_timer.go new file mode 100644 index 00000000..315e1715 --- /dev/null +++ b/service/oomkiller/service_timer.go @@ -0,0 +1,158 @@ +package oomkiller + +import ( + runtimeDebug "runtime/debug" + "sync" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/memory" +) + +const ( + defaultChecksBeforeLimit = 4 + defaultMinInterval = 500 * time.Millisecond + defaultMaxInterval = 10 * time.Second + defaultSafetyMargin = 5 * 1024 * 1024 +) + +type adaptiveTimer struct { + logger log.ContextLogger + router adapter.Router + memoryLimit uint64 + safetyMargin uint64 + minInterval time.Duration + maxInterval time.Duration + checksBeforeLimit int + useAvailable bool + + access sync.Mutex + timer *time.Timer + previousUsage uint64 + lastInterval time.Duration +} + +type timerConfig struct { + memoryLimit uint64 + safetyMargin uint64 + minInterval time.Duration + maxInterval time.Duration + checksBeforeLimit int + useAvailable bool +} + +func newAdaptiveTimer(logger log.ContextLogger, router adapter.Router, config timerConfig) *adaptiveTimer { + return &adaptiveTimer{ + logger: logger, + router: router, + memoryLimit: config.memoryLimit, + safetyMargin: config.safetyMargin, + minInterval: config.minInterval, + maxInterval: config.maxInterval, + checksBeforeLimit: config.checksBeforeLimit, + useAvailable: config.useAvailable, + } +} + +func (t *adaptiveTimer) start(_ uint64) { + t.access.Lock() + defer t.access.Unlock() + t.startLocked() +} + +func (t *adaptiveTimer) startNow() { + t.access.Lock() + t.startLocked() + t.access.Unlock() + t.poll() +} + +func (t *adaptiveTimer) startLocked() { + if t.timer != nil { + return + } + t.previousUsage = memory.Total() + t.lastInterval = t.minInterval + t.timer = time.AfterFunc(t.minInterval, t.poll) +} + +func (t *adaptiveTimer) stop() { + t.access.Lock() + defer t.access.Unlock() + t.stopLocked() +} + +func (t *adaptiveTimer) stopLocked() { + if t.timer != nil { + t.timer.Stop() + t.timer = nil + } +} + +func (t *adaptiveTimer) running() bool { + t.access.Lock() + defer t.access.Unlock() + return t.timer != nil +} + +func (t *adaptiveTimer) poll() { + t.access.Lock() + defer t.access.Unlock() + if t.timer == nil { + return + } + + usage := memory.Total() + delta := int64(usage) - int64(t.previousUsage) + t.previousUsage = usage + + var remaining uint64 + var triggered bool + + if t.memoryLimit > 0 { + if usage >= t.memoryLimit { + remaining = 0 + triggered = true + } else { + remaining = t.memoryLimit - usage + } + } else if t.useAvailable { + available := memory.Available() + if available <= t.safetyMargin { + remaining = 0 + triggered = true + } else { + remaining = available - t.safetyMargin + } + } else { + remaining = 0 + } + + if triggered { + t.logger.Error("memory threshold reached, usage: ", usage/(1024*1024), " MiB, resetting network") + t.router.ResetNetwork() + runtimeDebug.FreeOSMemory() + } + + var interval time.Duration + if triggered { + interval = t.maxInterval + } else if delta <= 0 { + interval = t.maxInterval + } else if t.checksBeforeLimit <= 0 { + interval = t.maxInterval + } else { + timeToLimit := time.Duration(float64(remaining) / float64(delta) * float64(t.lastInterval)) + interval = timeToLimit / time.Duration(t.checksBeforeLimit) + if interval < t.minInterval { + interval = t.minInterval + } + if interval > t.maxInterval { + interval = t.maxInterval + } + } + + t.lastInterval = interval + t.timer.Reset(interval) +} From 9c2cdc72030e64aafd8a4d8732e61118df9fa4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Feb 2026 21:48:39 +0800 Subject: [PATCH 05/12] Fix per-outbound bind_interface --- common/dialer/default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index ad37834c..aee14e99 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -90,7 +90,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial if networkManager != nil { defaultOptions := networkManager.DefaultOptions() - if defaultOptions.BindInterface != "" { + if defaultOptions.BindInterface != "" && !disableDefaultBind { bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) From 9d6dee7451e049af7905c7caf8bb9dc7ebc824f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Feb 2026 22:08:57 +0800 Subject: [PATCH 06/12] release: Fix pacman package --- .fpm_pacman | 23 +++++++++++++++++++++++ .github/workflows/build.yml | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .fpm_pacman diff --git a/.fpm_pacman b/.fpm_pacman new file mode 100644 index 00000000..8c86dfd9 --- /dev/null +++ b/.fpm_pacman @@ -0,0 +1,23 @@ +-s dir +--name sing-box +--category net +--license GPL-3.0-or-later +--description "The universal proxy platform." +--url "https://sing-box.sagernet.org/" +--maintainer "nekohasekai " +--config-files etc/sing-box/config.json +--after-install release/config/sing-box.postinst + +release/config/config.json=/etc/sing-box/config.json + +release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service +release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service +release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf +release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules +release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf + +release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash +release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish +release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box + +LICENSE=/usr/share/licenses/sing-box/LICENSE diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbd61b85..788b20af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -373,7 +373,7 @@ jobs: sudo gem install fpm sudo apt-get update sudo apt-get install -y libarchive-tools - cp .fpm_systemd .fpm + cp .fpm_pacman .fpm fpm -t pacman \ -v "$PKG_VERSION" \ -p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \ From 9bd9e9a58b65505edbf4aa1a929a4432e7b12d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 14:57:49 +0800 Subject: [PATCH 07/12] dialer: use KeepAliveConfig for TCP keepalive --- common/dialer/default.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index aee14e99..6b2379f4 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -158,8 +158,11 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial if keepInterval == 0 { keepInterval = C.TCPKeepAliveInterval } - dialer.KeepAlive = keepIdle - dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval)) + dialer.KeepAliveConfig = net.KeepAliveConfig{ + Enable: true, + Idle: keepIdle, + Interval: keepInterval, + } } var udpFragment bool if options.UDPFragment != nil { From fa3ab87b1133d4a3a8f0445a76fa9d1ff166bd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 15:00:06 +0800 Subject: [PATCH 08/12] platform: Fix gorelease build --- Makefile | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 88eb89c6..c30cd78f 100644 --- a/Makefile +++ b/Makefile @@ -249,8 +249,8 @@ lib_apple_new: $(SING_FFI) generate --config $(LIBBOX_FFI_CONFIG) --platform-type apple lib_install: - go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.11 - go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.11 + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.1.12 + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.1.12 docs: venv/bin/mkdocs serve diff --git a/go.mod b/go.mod index 37724e7e..54fb0c72 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64 github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64 github.com/sagernet/fswatch v0.1.1 - github.com/sagernet/gomobile v0.1.11 + github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 github.com/sagernet/sing v0.8.0-beta.16.0.20260227013657-e419e9875a07 diff --git a/go.sum b/go.sum index 8c683a95..5223e4e6 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,8 @@ github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe77 github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gomobile v0.1.11 h1:niMQAspvuThup5eRZQpsGcbM76zAvnsGr7RUIpnQMDQ= -github.com/sagernet/gomobile v0.1.11/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= +github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= +github.com/sagernet/gomobile v0.1.12/go.mod h1:A8l3FlHi2D/+mfcd4HHvk5DGFPW/ShFb9jHP5VmSiDY= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o= github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= From 11dc5bcbe141be2bc339c5d85e7916a41a7b8f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 15:34:16 +0800 Subject: [PATCH 09/12] Fixes in cronet-go --- .github/CRONET_GO_VERSION | 2 +- go.mod | 62 +++++++++---------- go.sum | 124 +++++++++++++++++++------------------- option/naive.go | 17 +++--- 4 files changed, 104 insertions(+), 101 deletions(-) diff --git a/.github/CRONET_GO_VERSION b/.github/CRONET_GO_VERSION index 52565262..e438ee5d 100644 --- a/.github/CRONET_GO_VERSION +++ b/.github/CRONET_GO_VERSION @@ -1 +1 @@ -34ec1a064c64f274c4e70bf7a9c7de4bb12331f6 +17c7ef18afa63b205e835c6270277b29382eb8e3 diff --git a/go.mod b/go.mod index 54fb0c72..4dd7fef8 100644 --- a/go.mod +++ b/go.mod @@ -27,8 +27,8 @@ require ( github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/cors v1.2.1 - github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64 - github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64 + github.com/sagernet/cronet-go v0.0.0-20260227112944-17c7ef18afa6 + github.com/sagernet/cronet-go/all v0.0.0-20260227112944-17c7ef18afa6 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/gomobile v0.1.12 github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 @@ -105,35 +105,35 @@ require ( github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717 // indirect - github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717 // indirect + github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260227112350-bf468eec914d // indirect + github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260227112350-bf468eec914d // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index 5223e4e6..2bae7570 100644 --- a/go.sum +++ b/go.sum @@ -152,68 +152,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= -github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64 h1:2hcUvxlEbzs4hbt9l0oeBpUfet2PHzaV6X6zNTDMkvQ= -github.com/sagernet/cronet-go v0.0.0-20260226034600-34ec1a064c64/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= -github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64 h1:f5TU9VAbhtm/SMLKctjMiSNUoQso4S6kEPUm5yNglNo= -github.com/sagernet/cronet-go/all v0.0.0-20260226034600-34ec1a064c64/go.mod h1:ohbVnfJvBzdv7R87Nz0fAmkLSTF9lCKrQC2rIqsQPY4= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717 h1:vtE5zVWFQuNls2jHb5edFCFYJblGY9qrqTVpoW2V/h4= -github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260226033953-5745aabe7717/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717 h1:+d5efMdtHbizXb4Vuai/11uNpWXmwOCfVgQcICA3kgk= -github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717 h1:6BYObS6w5sWnNLChKT1fOYJnJt/+p6du8fSyDb3DLwo= -github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260226033953-5745aabe7717/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717 h1:UbgjfMai+duPsftAT94zr4KcnACuJhjaxQkpup6TWLk= -github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717 h1:KRrzFzsWWgy8XPHfp2hEX7wTuc7ILQZiFKIATvuyCHU= -github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717 h1:bTp8j/GqUsS0P2aaTcbs7alSvUoywx+YIWyoCcVA05I= -github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717 h1:PKuCYfGpSfnzgRJjRR5em/yU5SoI4Yvx3r0C2k1Ksh4= -github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717 h1:w5ytvTm42ncUIFbLueN/ilp2ZaHrr5GC76Kc/Oxw4qI= -github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717 h1:2BjQH02cCF6spz/WG8bJHBPhZtF4Or5D73L+n5fZbNE= -github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717 h1:N9KUwDODj5eP5uqdSmbMufml2nGPc7oX4H5NPAjADIs= -github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260226033953-5745aabe7717/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717 h1:v2p6tqpQVI6RR1HamuJmkz01Ght4miQ79YhzQtOYowk= -github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717 h1:glPUIWUUAlOII+L0UI7/JAJ8o0wqAT7qVmWB4AcPyWs= -github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717 h1:0QKgSc5nBrR6H1ImsOlaBbVQ7onoFhwxTBwiOnUn0es= -github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717 h1:DsrfMoWf05lLrwdo0s/6wAC/4smeDgT4+t/iJtwGD+g= -github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260226033953-5745aabe7717/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717 h1:bYqI37NWgvxVBeWL50/ts23uGiyHv7QfH/Ylu5lvgLs= -github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717 h1:zg8keyQA2sniD4gqMwbUVdf57M1cmGobumOtZy1CYJs= -github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717 h1:cgyVkTyccMGiCayGgcvfc+q0b5GdhqSk+3UMmGhlCi0= -github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717 h1:anuE3B73oIvDOGJGvenfkQTm3NY2vL84aM062t8DZ4Y= -github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717 h1:kYN/amWzLOZMm5evPwlmumnswBNPeFZiKINKP73AsuA= -github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717 h1:S7rlC1Knzs3+It2u6AnhGUgiLU1QUzvoV3JXWvwE4n8= -github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260226033953-5745aabe7717/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717 h1:FeyB5ZyFHAWkU1DETmUjLEM9GUFWoIndV8usT4AjGOk= -github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260226033953-5745aabe7717/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717 h1:loU7PsyqI7dplbFGjQKoMT4IVYuzVyfW88FIq+7gbmY= -github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717 h1:XSVrZ18XauiYMf+G0CzZETteQF0r1NOoA1gVXlZWjUI= -github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717 h1:oTRK4T5pQ1ZYQh7Avr5u3bsDnXkloSVqwBljRy8FHOY= -github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260226033953-5745aabe7717/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717 h1:J18xRmUa9lle+Y6pYkQ7OMq2B9eox5Bmm4MkbuQPQm0= -github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717 h1:PC9gKeDN3wmcFxwiBEM57W0YwEz1uFGqawwssEQe494= -github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717 h1:STlbMnsydaymby26m3wNfeYYI7HoiZ6+wxQ75Si1g8E= -github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260226033953-5745aabe7717/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717 h1:qWtpoBxmHyb49IRtMAFkJIRlL4KCYW5Fe6YptYhItpM= -github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717 h1:7ohuWJ0PswrU7Xw/xfL6OrIh7sXybGNOgh6DaL+urdY= -github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260226033953-5745aabe7717/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= +github.com/sagernet/cronet-go v0.0.0-20260227112944-17c7ef18afa6 h1:Ato+guxmEL4uezcYV1UUUDpAv9HlcJQ7BZt2zpnzjuw= +github.com/sagernet/cronet-go v0.0.0-20260227112944-17c7ef18afa6/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw= +github.com/sagernet/cronet-go/all v0.0.0-20260227112944-17c7ef18afa6 h1:0ldSjcR5Gt/o+otTvUAmJ28FCLab9lnlpEhxRCMQpRA= +github.com/sagernet/cronet-go/all v0.0.0-20260227112944-17c7ef18afa6/go.mod h1:xVwYoNCyv9tF7W1RJlUdDbT4bn5tyqtyTe1P1ZY2VP8= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260227112350-bf468eec914d h1:tudlBYdQHIWctKIdf7pceBOFIUIISK6yFivwsxhxDk0= +github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260227112350-bf468eec914d/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260227112350-bf468eec914d h1:F5EsQlIknj0HlExBFR4EXW69dYj0MpK1HCpKhL/weEs= +github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260227112350-bf468eec914d h1:9SQ6I2Y2radd6RyWEfV+9s1Q9Kth54B6gBHuJWNzQas= +github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260227112350-bf468eec914d/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260227112350-bf468eec914d h1:+XoeknBi6+s6epDAS3BkEsp5zGqEJsT9L8JEcaq+0nE= +github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260227112350-bf468eec914d h1:poqfhHJAg+7BtABn4cue7V4y8Kb2eZ1Cy0j+bhDangw= +github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260227112350-bf468eec914d h1:nH6rtfqWbui9zQPjd18cpvZncGvn21UcVLtmeUoQKXs= +github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260227112350-bf468eec914d h1:HtnjWZzSQBaP29XJ5NoIps1TVZ7DUC7R0NH7IyhJ5Ag= +github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260227112350-bf468eec914d/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260227112350-bf468eec914d h1:E2DWx0Agrj8Fi745S+otYW+W0rL2I8+Z2rZCFqGYPvQ= +github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260227112350-bf468eec914d h1:j7f/rBwPlO1RpFQeM35QVHymVXGVo6d8WTz4i4SjcPo= +github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260227112350-bf468eec914d/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260227112350-bf468eec914d h1:hz8kkcHGMe7QBTpbqkaw89ZFsfX+UN5F5RIDrroDkx8= +github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260227112350-bf468eec914d/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260227112350-bf468eec914d h1:TNFaO19ySEyqG79j5+dYb+w4ivusrTXanWuogmC4VM0= +github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260227112350-bf468eec914d h1:Ewc/wR3yu/hOwG/p49nI9TwYmYv3Llm5DA6fSb1w8hY= +github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260227112350-bf468eec914d h1:PJ24NkPNpMrLGNRdb6moEqJo8gfhYcIRZmQD8jPPCJk= +github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260227112350-bf468eec914d h1:IaUghNA8cOmmwvzUPKPsfhiG0KmpWpE0mFZl85T5/Bw= +github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260227112350-bf468eec914d/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260227112350-bf468eec914d h1:whbeDcr9dDWPr45Is9QV6OHAncrBWLJtPuo4uyEJFBg= +github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260227112350-bf468eec914d h1:ecHgaGMvikNYjsfULekdXjL/cQJXCS38yvHaKVMWtXc= +github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260227112350-bf468eec914d h1:no7Cb54+vv1bQ39zFp+JIHKO8Tu3sTwqz8SoOAuV/Ek= +github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260227112350-bf468eec914d h1:DqBSbam9KAzBgDInOoNy4K0baSJyxGWESxrDewU5aSs= +github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260227112350-bf468eec914d h1:fOR5i+hRyjG8ZzPSG6URkoTKr5qYOJfxZ58zd8HBteM= +github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260227112350-bf468eec914d h1:hEQGQI+PfUzYBVas4NWw8WiEUsATco6vwv+t4qTtgtw= +github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260227112350-bf468eec914d/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260227112350-bf468eec914d h1:AzzJ5AtZlwTbU5QOSixZdPLTjzWKCun3AobQChKy0W8= +github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260227112350-bf468eec914d/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260227112350-bf468eec914d h1:9Tp7s/WX4DZLx4ues8G38G2OV7eQbeuU2COEZEbGcF0= +github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260227112350-bf468eec914d h1:T9EVZKTyZHOamwevomUZnJ6TQNc09I/BwK+L5HJCJj8= +github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260227112350-bf468eec914d h1:FZmThI7xScJRPERFiA4L2l9KCwA0oi8/lEOajIKEtUQ= +github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260227112350-bf468eec914d/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260227112350-bf468eec914d h1:BCC/b8bL0dD9Q4ghgKABV/EsMe0J8GE/l7hcRdPkUXQ= +github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260227112350-bf468eec914d/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260227112350-bf468eec914d h1:3l463BXnC/X42ow2zqHm9Y/K4GM6aRsKUIZBcFxr2+Q= +github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260227112350-bf468eec914d h1:+XHEZ/z5NgPfjOAzOwfbQzR+42qaDNB0nv+fAOcd6Pc= +github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260227112350-bf468eec914d/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260227112350-bf468eec914d h1:sYWbP+qCt9Rhb1yGaIRY7HVLtaQZmrHWR0obc5+Q1qc= +github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260227112350-bf468eec914d h1:r6eOVlAfmcUMD5nfz+mPd/aORevUKhcvxA1z1GdPnG8= +github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260227112350-bf468eec914d/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg= diff --git a/option/naive.go b/option/naive.go index fcc315b6..da3a88db 100644 --- a/option/naive.go +++ b/option/naive.go @@ -2,6 +2,7 @@ package option import ( "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/byteformats" "github.com/sagernet/sing/common/json/badoption" ) @@ -26,12 +27,14 @@ type NaiveInboundOptions struct { type NaiveOutboundOptions struct { DialerOptions ServerOptions - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - InsecureConcurrency int `json:"insecure_concurrency,omitempty"` - ExtraHeaders badoption.HTTPHeader `json:"extra_headers,omitempty"` - UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` - QUIC bool `json:"quic,omitempty"` - QUICCongestionControl string `json:"quic_congestion_control,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + InsecureConcurrency int `json:"insecure_concurrency,omitempty"` + ExtraHeaders badoption.HTTPHeader `json:"extra_headers,omitempty"` + ReceiveWindow *byteformats.MemoryBytes `json:"stream_receive_window,omitempty"` + UDPOverTCP *UDPOverTCPOptions `json:"udp_over_tcp,omitempty"` + QUIC bool `json:"quic,omitempty"` + QUICCongestionControl string `json:"quic_congestion_control,omitempty"` + QUICSessionReceiveWindow *byteformats.MemoryBytes `json:"quic_session_receive_window,omitempty"` OutboundTLSOptionsContainer } From 93b7328c3fdbabc07f981e305f6050dca94b50f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 18:18:33 +0800 Subject: [PATCH 10/12] Fix missing Tailscale in ProxyDisplayName --- constant/proxy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/constant/proxy.go b/constant/proxy.go index 4130c631..278a46c2 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -86,6 +86,8 @@ func ProxyDisplayName(proxyType string) string { return "Hysteria2" case TypeAnyTLS: return "AnyTLS" + case TypeTailscale: + return "Tailscale" case TypeSelector: return "Selector" case TypeURLTest: From 13e6ba4cb23147d68108652fd4f5e71009802428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 19:55:27 +0800 Subject: [PATCH 11/12] Update tfo-go --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4dd7fef8..0621134c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/caddyserver/certmagic v0.25.0 github.com/coder/websocket v1.8.14 github.com/cretz/bine v0.2.0 - github.com/database64128/tfo-go/v2 v2.3.1 + github.com/database64128/tfo-go/v2 v2.3.2 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/render v1.0.3 github.com/godbus/dbus/v5 v5.2.1 @@ -54,7 +54,7 @@ require ( golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 golang.org/x/mod v0.31.0 golang.org/x/net v0.48.0 - golang.org/x/sys v0.39.0 + golang.org/x/sys v0.41.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.11 diff --git a/go.sum b/go.sum index 2bae7570..987a87f6 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM= github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc= -github.com/database64128/tfo-go/v2 v2.3.1 h1:EGE+ELd5/AQ0X6YBlQ9RgKs8+kciNhgN3d8lRvfEJQw= -github.com/database64128/tfo-go/v2 v2.3.1/go.mod h1:k9wcpg/8i5zenspBkc9jUEYehpZZccBnCElzOJB++bU= +github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw= +github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -346,8 +346,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= From 6da7e538e122b77c226ce48c1a6b4ecb61fd3fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 27 Feb 2026 15:08:52 +0800 Subject: [PATCH 12/12] Bump version --- clients/android | 2 +- clients/apple | 2 +- docs/changelog.md | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/clients/android b/clients/android index 4bdde0ae..7d1e7c72 160000 --- a/clients/android +++ b/clients/android @@ -1 +1 @@ -Subproject commit 4bdde0ae4db2f0fe041a52520e12315141d0981c +Subproject commit 7d1e7c72cebdce23ea4f5f3dcfc8d12a4dc29633 diff --git a/clients/apple b/clients/apple index 015dcd26..80c86686 160000 --- a/clients/apple +++ b/clients/apple @@ -1 +1 @@ -Subproject commit 015dcd266ba8651f5be20de531bf9184470f750d +Subproject commit 80c866861df86b43d597e86b086458ff8d6c103b diff --git a/docs/changelog.md b/docs/changelog.md index d3162824..b67663e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,9 +2,7 @@ icon: material/alert-decagram --- -#### 1.13.0-rc.6 - -* Fixes and improvements +#### 1.13.0 Important changes since 1.12: @@ -22,7 +20,7 @@ Important changes since 1.12: * Improve `local` DNS server **12** * Add `disable_tcp_keep_alive`, `tcp_keep_alive` and `tcp_keep_alive_interval` options for listen and dial fields **13** * Add `bind_address_no_port` option for dial fields **14** -* Add system interface and relay server options for Tailscale endpoint **15** +* Add system interface, relay server and advertise tags options for Tailscale endpoint **15** * Add Claude Code Multiplexer service **16** * Add OpenAI Codex Multiplexer service **17** * Apple/Android: Refactor GUI @@ -136,6 +134,7 @@ See [Dial Fields](/configuration/shared/dial/#bind_address_no_port). Tailscale endpoint can now create a system TUN interface to handle traffic directly. New `relay_server_port` and `relay_server_static_endpoints` options for incoming relay connections. +New `advertise_tags` option for ACL tag advertisement. See [Tailscale endpoint](/configuration/endpoint/tailscale/). @@ -169,6 +168,10 @@ Also, documentation has been updated with a warning about uTLS fingerprinting vu uTLS is not recommended for censorship circumvention due to fundamental architectural limitations; use NaiveProxy instead for TLS fingerprint resistance. +#### 1.12.23 + +* Fixes and improvements + #### 1.13.0-rc.5 * Add `mipsle`, `mips64le`, `riscv64` and `loong64` support for NaiveProxy outbound