From ab1412a0c93643f8e41934ff304dcee887a8bb9f Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:37:59 +0800 Subject: [PATCH 1/6] feat: add bandwidth limiter --- adapter/router.go | 1 + box.go | 4 + docs/configuration/index.md | 2 + docs/configuration/index.zh.md | 2 + docs/configuration/limiter/index.md | 50 ++++++++++++ docs/configuration/limiter/index.zh.md | 50 ++++++++++++ docs/configuration/route/rule.md | 16 +++- docs/configuration/route/rule.zh.md | 16 +++- limiter/builder.go | 106 +++++++++++++++++++++++++ limiter/limiter.go | 77 ++++++++++++++++++ limiter/manager.go | 11 +++ option/config.go | 1 + option/limiter.go | 9 +++ option/rule.go | 10 ++- route/router.go | 17 ++++ route/rule_abstract.go | 10 +++ route/rule_default.go | 6 ++ 17 files changed, 380 insertions(+), 8 deletions(-) create mode 100644 docs/configuration/limiter/index.md create mode 100644 docs/configuration/limiter/index.zh.md create mode 100644 limiter/builder.go create mode 100644 limiter/limiter.go create mode 100644 limiter/manager.go create mode 100644 option/limiter.go diff --git a/adapter/router.go b/adapter/router.go index ec23d9311f..e6533bd1cf 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -70,6 +70,7 @@ type Rule interface { Match(metadata *InboundContext) bool Outbound() string String() string + Limiters() []string } type DNSRule interface { diff --git a/box.go b/box.go index 0a66c88c3a..ac1625a88a 100644 --- a/box.go +++ b/box.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/inbound" + "github.com/sagernet/sing-box/limiter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/outbound" @@ -76,6 +77,9 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create log factory") } + if len(options.Limiters) > 0 { + ctx = limiter.WithDefault(ctx, logFactory.NewLogger("limiter"), options.Limiters) + } router, err := route.NewRouter( ctx, logFactory, diff --git a/docs/configuration/index.md b/docs/configuration/index.md index a4ade70726..05208ebb64 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -11,6 +11,7 @@ sing-box uses JSON for configuration files. "ntp": {}, "inbounds": [], "outbounds": [], + "limiters": [], "route": {}, "experimental": {} } @@ -25,6 +26,7 @@ sing-box uses JSON for configuration files. | `ntp` | [NTP](./ntp) | | `inbounds` | [Inbound](./inbound) | | `outbounds` | [Outbound](./outbound) | +| `limiters` | [Limiter](./limiter) | | `route` | [Route](./route) | | `experimental` | [Experimental](./experimental) | diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md index 80b2ebd385..e532bd6234 100644 --- a/docs/configuration/index.zh.md +++ b/docs/configuration/index.zh.md @@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。 "dns": {}, "inbounds": [], "outbounds": [], + "limiters": [], "route": {}, "experimental": {} } @@ -23,6 +24,7 @@ sing-box 使用 JSON 作为配置文件格式。 | `dns` | [DNS](./dns) | | `inbounds` | [入站](./inbound) | | `outbounds` | [出站](./outbound) | +| `limiters` | [限速](./limiter) | | `route` | [路由](./route) | | `experimental` | [实验性](./experimental) | diff --git a/docs/configuration/limiter/index.md b/docs/configuration/limiter/index.md new file mode 100644 index 0000000000..2c89b26d1b --- /dev/null +++ b/docs/configuration/limiter/index.md @@ -0,0 +1,50 @@ +# Limiter + +### Structure + +```json +{ + "limiters": [ + { + "tag": "limiter-a", + "download": "1M", + "upload": "10M", + "auth_user": [ + "user-a", + "user-b" + ], + "inbound": [ + "in-a", + "in-b" + ] + } + ] +} + +``` + +### Fields + +#### download upload + +==Required== + +Format: `[Integer][Unit]` e.g. `100M, 100m, 1G, 1g`. + +Supported units (case insensitive): `B, K, M, G, T, P, E`. + +#### tag + +The tag of the limiter, used in route rule. + +#### auth_user + +Global limiter for a group of usernames, see each inbound for details. + +#### inbound + +Global limiter for a group of inbounds. + +!!! info "" + + All the auth_users, inbounds and route rule with limiter tag share the same limiter. To take effect independently, configure limiters seperately. \ No newline at end of file diff --git a/docs/configuration/limiter/index.zh.md b/docs/configuration/limiter/index.zh.md new file mode 100644 index 0000000000..cb4b175abc --- /dev/null +++ b/docs/configuration/limiter/index.zh.md @@ -0,0 +1,50 @@ +# 限速 + +### 结构 + +```json +{ + "limiters": [ + { + "tag": "limiter-a", + "download": "1M", + "upload": "10M", + "auth_user": [ + "user-a", + "user-b" + ], + "inbound": [ + "in-a", + "in-b" + ] + } + ] +} + +``` + +### 字段 + +#### download upload + +==必填== + +格式: `[Integer][Unit]` 例如: `100M, 100m, 1G, 1g`. + +支持的单位 (大小写不敏感): `B, K, M, G, T, P, E`. + +#### tag + +限速标签,在路由规则中使用。 + +#### auth_user + +用户组全局限速,参阅入站设置。 + +#### inbound + +入站组全局限速。 + +!!! info "" + + 所有用户、入站和有限速标签的路由规则共享同一个限速。为了独立生效,请分别配置限速器。 \ No newline at end of file diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 3cee478dc3..77ce35ec6a 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -84,14 +84,22 @@ ], "clash_mode": "direct", "invert": false, - "outbound": "direct" + "outbound": "direct", + "limiter": [ + "limiter-a", + "limiter-b" + ] }, { "type": "logical", "mode": "and", "rules": [], "invert": false, - "outbound": "direct" + "outbound": "direct", + "limiter": [ + "limiter-a", + "limiter-b" + ] } ] } @@ -238,6 +246,10 @@ Invert match result. Tag of the target outbound. +#### limiter + +Tags of [Limiter](/configuration/limiter). Take effect for all connections matching this rule. + ### Logical Fields #### type diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 4a09ed8e64..1bdb83071b 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -82,14 +82,22 @@ ], "clash_mode": "direct", "invert": false, - "outbound": "direct" + "outbound": "direct", + "limiter": [ + "limiter-a", + "limiter-b" + ] }, { "type": "logical", "mode": "and", "rules": [], "invert": false, - "outbound": "direct" + "outbound": "direct", + "limiter": [ + "limiter-a", + "limiter-b" + ] } ] } @@ -236,6 +244,10 @@ 目标出站的标签。 +#### limiter + +[限速](/zh/configuration/inbound) 标签。对所有匹配该规则的连接生效。 + ### 逻辑字段 #### type diff --git a/limiter/builder.go b/limiter/builder.go new file mode 100644 index 0000000000..6713797c7a --- /dev/null +++ b/limiter/builder.go @@ -0,0 +1,106 @@ +package limiter + +import ( + "context" + "fmt" + "net" + "sync" + + "github.com/sagernet/sing-box/common/humanize" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" +) + +const ( + limiterTag = "tag" + limiterUser = "user" + limiterInbound = "inbound" +) + +var _ Manager = (*defaultManager)(nil) + +type defaultManager struct { + mp *sync.Map +} + +func WithDefault(ctx context.Context, logger log.ContextLogger, options []option.Limiter) context.Context { + m := &defaultManager{mp: &sync.Map{}} + for i, option := range options { + if err := m.createLimiter(ctx, option); err != nil { + logger.ErrorContext(ctx, fmt.Sprintf("id=%d, %s", i, err)) + } else { + logger.InfoContext(ctx, fmt.Sprintf("id=%d, tag=%s, users=%v, inbounds=%v, download=%s, upload=%s", + i, option.Tag, option.AuthUser, option.Inbound, option.Download, option.Upload)) + } + } + return service.ContextWith[Manager](ctx, m) +} + +func buildKey(prefix string, tag string) string { + return fmt.Sprintf("%s|%s", prefix, tag) +} + +func (m *defaultManager) createLimiter(ctx context.Context, option option.Limiter) (err error) { + var download, upload uint64 + if len(option.Download) > 0 { + download, err = humanize.ParseBytes(option.Download) + if err != nil { + return err + } + } + if len(option.Upload) > 0 { + upload, err = humanize.ParseBytes(option.Upload) + if err != nil { + return err + } + } + if download == 0 && upload == 0 { + return E.New("download/upload, at least one must be set") + } + l := newLimiter(download, upload) + valid := false + if len(option.Tag) > 0 { + valid = true + m.mp.Store(buildKey(limiterTag, option.Tag), l) + } + if len(option.AuthUser) > 0 { + valid = true + for _, user := range option.AuthUser { + m.mp.Store(buildKey(limiterUser, user), l) + } + } + if len(option.Inbound) > 0 { + valid = true + for _, inbound := range option.Inbound { + m.mp.Store(buildKey(limiterInbound, inbound), l) + } + } + if !valid { + return E.New("tag/user/inbound, at least one must be set") + } + return +} + +func (m *defaultManager) LoadLimiters(tags []string, user, inbound string) (limiters []*limiter) { + for _, t := range tags { + if v, ok := m.mp.Load(buildKey(limiterTag, t)); ok { + limiters = append(limiters, v.(*limiter)) + } + } + if v, ok := m.mp.Load(buildKey(limiterUser, user)); ok { + limiters = append(limiters, v.(*limiter)) + } + if v, ok := m.mp.Load(buildKey(limiterInbound, inbound)); ok { + limiters = append(limiters, v.(*limiter)) + } + return +} + +func (m *defaultManager) NewConnWithLimiters(ctx context.Context, conn net.Conn, limiters []*limiter) net.Conn { + for _, limiter := range limiters { + conn = &connWithLimiter{Conn: conn, limiter: limiter, ctx: ctx} + } + return conn +} diff --git a/limiter/limiter.go b/limiter/limiter.go new file mode 100644 index 0000000000..8679c558ae --- /dev/null +++ b/limiter/limiter.go @@ -0,0 +1,77 @@ +package limiter + +import ( + "context" + "net" + + "golang.org/x/time/rate" +) + +type limiter struct { + downloadLimiter *rate.Limiter + uploadLimiter *rate.Limiter +} + +func newLimiter(download, upload uint64) *limiter { + var downloadLimiter, uploadLimiter *rate.Limiter + if download > 0 { + downloadLimiter = rate.NewLimiter(rate.Limit(float64(download)), int(download)) + } + if upload > 0 { + uploadLimiter = rate.NewLimiter(rate.Limit(float64(upload)), int(upload)) + } + return &limiter{downloadLimiter: downloadLimiter, uploadLimiter: uploadLimiter} +} + +type connWithLimiter struct { + net.Conn + limiter *limiter + ctx context.Context +} + +func (conn *connWithLimiter) Read(p []byte) (n int, err error) { + if conn.limiter == nil || conn.limiter.downloadLimiter == nil { + return conn.Conn.Read(p) + } + b := conn.limiter.downloadLimiter.Burst() + if b < len(p) { + p = p[:b] + } + n, err = conn.Conn.Read(p) + if err != nil { + return + } + err = conn.limiter.downloadLimiter.WaitN(conn.ctx, n) + if err != nil { + return + } + return +} + +func (conn *connWithLimiter) Write(p []byte) (n int, err error) { + if conn.limiter == nil || conn.limiter.uploadLimiter == nil { + return conn.Conn.Write(p) + } + var nn int + b := conn.limiter.uploadLimiter.Burst() + for { + end := len(p) + if end == 0 { + break + } + if b < len(p) { + end = b + } + err = conn.limiter.uploadLimiter.WaitN(conn.ctx, end) + if err != nil { + return + } + nn, err = conn.Conn.Write(p[:end]) + n += nn + if err != nil { + return + } + p = p[end:] + } + return +} diff --git a/limiter/manager.go b/limiter/manager.go new file mode 100644 index 0000000000..4521393074 --- /dev/null +++ b/limiter/manager.go @@ -0,0 +1,11 @@ +package limiter + +import ( + "context" + "net" +) + +type Manager interface { + LoadLimiters(tags []string, user, inbound string) []*limiter + NewConnWithLimiters(ctx context.Context, conn net.Conn, limiters []*limiter) net.Conn +} diff --git a/option/config.go b/option/config.go index ec471112e7..ddb9ddec10 100644 --- a/option/config.go +++ b/option/config.go @@ -16,6 +16,7 @@ type _Options struct { Inbounds []Inbound `json:"inbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"` Route *RouteOptions `json:"route,omitempty"` + Limiters []Limiter `json:"limiters,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"` } diff --git a/option/limiter.go b/option/limiter.go new file mode 100644 index 0000000000..99f1dc629b --- /dev/null +++ b/option/limiter.go @@ -0,0 +1,9 @@ +package option + +type Limiter struct { + Tag string `json:"tag"` + Download string `json:"download,omitempty"` + Upload string `json:"upload,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` +} diff --git a/option/rule.go b/option/rule.go index f78a752d91..c813eea92e 100644 --- a/option/rule.go +++ b/option/rule.go @@ -80,6 +80,7 @@ type DefaultRule struct { ClashMode string `json:"clash_mode,omitempty"` Invert bool `json:"invert,omitempty"` Outbound string `json:"outbound,omitempty"` + Limiter Listable[string] `json:"limiter,omitempty"` } func (r DefaultRule) IsValid() bool { @@ -90,10 +91,11 @@ func (r DefaultRule) IsValid() bool { } type LogicalRule struct { - Mode string `json:"mode"` - Rules []DefaultRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + Mode string `json:"mode"` + Rules []DefaultRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` + Outbound string `json:"outbound,omitempty"` + Limiter Listable[string] `json:"limiter,omitempty"` } func (r LogicalRule) IsValid() bool { diff --git a/route/router.go b/route/router.go index 9b302d5e1a..e2a79fc586 100644 --- a/route/router.go +++ b/route/router.go @@ -21,6 +21,7 @@ import ( "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" + "github.com/sagernet/sing-box/limiter" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/ntp" "github.com/sagernet/sing-box/option" @@ -85,6 +86,7 @@ type Router struct { pauseManager pause.Manager clashServer adapter.ClashServer v2rayServer adapter.V2RayServer + limiterManager limiter.Manager platformInterface platform.Interface } @@ -498,6 +500,9 @@ func (r *Router) Start() error { return E.Cause(err, "initialize time service") } } + if limiterManger := service.FromContext[limiter.Manager](r.ctx); limiterManger != nil { + r.limiterManager = limiterManger + } return nil } @@ -701,6 +706,18 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if !common.Contains(detour.Network(), N.NetworkTCP) { return E.New("missing supported outbound, closing connection") } + + if r.limiterManager != nil { + var limiterTags []string + if matchedRule != nil { + limiterTags = matchedRule.Limiters() + } + limiters := r.limiterManager.LoadLimiters(limiterTags, metadata.User, metadata.Inbound) + if len(limiters) > 0 { + conn = r.limiterManager.NewConnWithLimiters(ctx, conn, limiters) + } + } + if r.clashServer != nil { trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule) defer tracker.Leave() diff --git a/route/rule_abstract.go b/route/rule_abstract.go index 38d4d57d41..bbbbe99f02 100644 --- a/route/rule_abstract.go +++ b/route/rule_abstract.go @@ -18,6 +18,7 @@ type abstractDefaultRule struct { allItems []RuleItem invert bool outbound string + limiters []string } func (r *abstractDefaultRule) Type() string { @@ -126,6 +127,10 @@ func (r *abstractDefaultRule) Outbound() string { return r.outbound } +func (r *abstractDefaultRule) Limiters() []string { + return r.limiters +} + func (r *abstractDefaultRule) String() string { if !r.invert { return strings.Join(F.MapToString(r.allItems), " ") @@ -139,6 +144,7 @@ type abstractLogicalRule struct { mode string invert bool outbound string + limiters []string } func (r *abstractLogicalRule) Type() string { @@ -191,6 +197,10 @@ func (r *abstractLogicalRule) Outbound() string { return r.outbound } +func (r *abstractLogicalRule) Limiters() []string { + return r.limiters +} + func (r *abstractLogicalRule) String() string { var op string switch r.mode { diff --git a/route/rule_default.go b/route/rule_default.go index 01322c13aa..780fd8cc7c 100644 --- a/route/rule_default.go +++ b/route/rule_default.go @@ -184,6 +184,9 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.Limiter) > 0 { + rule.limiters = append(rule.limiters, options.Limiter...) + } return rule, nil } @@ -216,5 +219,8 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt } r.rules[i] = rule } + if len(options.Limiter) > 0 { + r.limiters = append(r.limiters, options.Limiter...) + } return r, nil } From 077b77a4a54324d8fedf3535275bf9209a662efb Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Mon, 24 Jul 2023 11:39:51 +0800 Subject: [PATCH 2/6] refractor: limiter builder 1. use map instead of sync.Map in defaultManger for concurrent read only 2. build limiterKey with struct --- limiter/builder.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/limiter/builder.go b/limiter/builder.go index 6713797c7a..6ef7e59df3 100644 --- a/limiter/builder.go +++ b/limiter/builder.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net" - "sync" "github.com/sagernet/sing-box/common/humanize" "github.com/sagernet/sing-box/log" @@ -14,19 +13,24 @@ import ( ) const ( - limiterTag = "tag" - limiterUser = "user" - limiterInbound = "inbound" + prefixTag = "tag" + prefixUser = "user" + prefixInbound = "inbound" ) var _ Manager = (*defaultManager)(nil) +type limiterKey struct { + Prefix string + Name string +} + type defaultManager struct { - mp *sync.Map + mp map[limiterKey]*limiter } func WithDefault(ctx context.Context, logger log.ContextLogger, options []option.Limiter) context.Context { - m := &defaultManager{mp: &sync.Map{}} + m := &defaultManager{mp: make(map[limiterKey]*limiter)} for i, option := range options { if err := m.createLimiter(ctx, option); err != nil { logger.ErrorContext(ctx, fmt.Sprintf("id=%d, %s", i, err)) @@ -38,10 +42,6 @@ func WithDefault(ctx context.Context, logger log.ContextLogger, options []option return service.ContextWith[Manager](ctx, m) } -func buildKey(prefix string, tag string) string { - return fmt.Sprintf("%s|%s", prefix, tag) -} - func (m *defaultManager) createLimiter(ctx context.Context, option option.Limiter) (err error) { var download, upload uint64 if len(option.Download) > 0 { @@ -63,18 +63,18 @@ func (m *defaultManager) createLimiter(ctx context.Context, option option.Limite valid := false if len(option.Tag) > 0 { valid = true - m.mp.Store(buildKey(limiterTag, option.Tag), l) + m.mp[limiterKey{prefixTag, option.Tag}] = l } if len(option.AuthUser) > 0 { valid = true for _, user := range option.AuthUser { - m.mp.Store(buildKey(limiterUser, user), l) + m.mp[limiterKey{prefixUser, user}] = l } } if len(option.Inbound) > 0 { valid = true for _, inbound := range option.Inbound { - m.mp.Store(buildKey(limiterInbound, inbound), l) + m.mp[limiterKey{prefixInbound, inbound}] = l } } if !valid { @@ -84,16 +84,16 @@ func (m *defaultManager) createLimiter(ctx context.Context, option option.Limite } func (m *defaultManager) LoadLimiters(tags []string, user, inbound string) (limiters []*limiter) { - for _, t := range tags { - if v, ok := m.mp.Load(buildKey(limiterTag, t)); ok { - limiters = append(limiters, v.(*limiter)) + for _, tag := range tags { + if v, ok := m.mp[limiterKey{prefixTag, tag}]; ok { + limiters = append(limiters, v) } } - if v, ok := m.mp.Load(buildKey(limiterUser, user)); ok { - limiters = append(limiters, v.(*limiter)) + if v, ok := m.mp[limiterKey{prefixUser, user}]; ok { + limiters = append(limiters, v) } - if v, ok := m.mp.Load(buildKey(limiterInbound, inbound)); ok { - limiters = append(limiters, v.(*limiter)) + if v, ok := m.mp[limiterKey{prefixInbound, inbound}]; ok { + limiters = append(limiters, v) } return } From df4a879a2ebd534e84e8a10daf268b3fee6c6a59 Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Mon, 24 Jul 2023 14:48:58 +0800 Subject: [PATCH 3/6] feat: independent limiter for user/inbound --- docs/configuration/limiter/index.md | 16 +++++++---- docs/configuration/limiter/index.zh.md | 16 +++++++---- limiter/builder.go | 39 ++++++++++++++------------ option/limiter.go | 12 ++++---- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/docs/configuration/limiter/index.md b/docs/configuration/limiter/index.md index 2c89b26d1b..6371d080d2 100644 --- a/docs/configuration/limiter/index.md +++ b/docs/configuration/limiter/index.md @@ -13,10 +13,12 @@ "user-a", "user-b" ], + "auth_user_independent": false, "inbound": [ "in-a", "in-b" - ] + ], + "inbound_independent": false } ] } @@ -39,12 +41,16 @@ The tag of the limiter, used in route rule. #### auth_user -Global limiter for a group of usernames, see each inbound for details. +Apply limiter for a group of usernames, see each inbound for details. + +#### auth_user_independent + +Make each auth_user's limiter independent. If disabled, the same limiter will be shared. #### inbound -Global limiter for a group of inbounds. +Apply limiter for a group of inbounds. -!!! info "" +#### inbound_independent - All the auth_users, inbounds and route rule with limiter tag share the same limiter. To take effect independently, configure limiters seperately. \ No newline at end of file +Make each inbound's limiter independent. If disabled, the same limiter will be shared. diff --git a/docs/configuration/limiter/index.zh.md b/docs/configuration/limiter/index.zh.md index cb4b175abc..5b71cf3de1 100644 --- a/docs/configuration/limiter/index.zh.md +++ b/docs/configuration/limiter/index.zh.md @@ -13,10 +13,12 @@ "user-a", "user-b" ], + "auth_user_independent": false, "inbound": [ "in-a", "in-b" - ] + ], + "inbound_independent": false } ] } @@ -39,12 +41,16 @@ #### auth_user -用户组全局限速,参阅入站设置。 +用户组限速,参阅入站设置。 + +#### auth_user_independent + +使每个用户有单独的限速。关闭时将共享限速。 #### inbound -入站组全局限速。 +入站组限速。 -!!! info "" +#### inbound_independent - 所有用户、入站和有限速标签的路由规则共享同一个限速。为了独立生效,请分别配置限速器。 \ No newline at end of file +使每个入站有单独的限速。关闭时将共享限速。 diff --git a/limiter/builder.go b/limiter/builder.go index 6ef7e59df3..ea16faee42 100644 --- a/limiter/builder.go +++ b/limiter/builder.go @@ -44,13 +44,13 @@ func WithDefault(ctx context.Context, logger log.ContextLogger, options []option func (m *defaultManager) createLimiter(ctx context.Context, option option.Limiter) (err error) { var download, upload uint64 - if len(option.Download) > 0 { + if option.Download != "" { download, err = humanize.ParseBytes(option.Download) if err != nil { return err } } - if len(option.Upload) > 0 { + if option.Upload != "" { upload, err = humanize.ParseBytes(option.Upload) if err != nil { return err @@ -59,26 +59,29 @@ func (m *defaultManager) createLimiter(ctx context.Context, option option.Limite if download == 0 && upload == 0 { return E.New("download/upload, at least one must be set") } - l := newLimiter(download, upload) - valid := false - if len(option.Tag) > 0 { - valid = true - m.mp[limiterKey{prefixTag, option.Tag}] = l + if option.Tag == "" && len(option.AuthUser) == 0 && len(option.Inbound) == 0 { + return E.New("tag/user/inbound, at least one must be set") } - if len(option.AuthUser) > 0 { - valid = true - for _, user := range option.AuthUser { - m.mp[limiterKey{prefixUser, user}] = l - } + var sharedLimiter *limiter + if option.Tag != "" || !option.AuthUserIndependent || !option.InboundIndependent { + sharedLimiter = newLimiter(download, upload) } - if len(option.Inbound) > 0 { - valid = true - for _, inbound := range option.Inbound { - m.mp[limiterKey{prefixInbound, inbound}] = l + if option.Tag != "" { + m.mp[limiterKey{prefixTag, option.Tag}] = sharedLimiter + } + for _, user := range option.AuthUser { + if option.AuthUserIndependent { + m.mp[limiterKey{prefixUser, user}] = newLimiter(download, upload) + } else { + m.mp[limiterKey{prefixUser, user}] = sharedLimiter } } - if !valid { - return E.New("tag/user/inbound, at least one must be set") + for _, inbound := range option.Inbound { + if option.InboundIndependent { + m.mp[limiterKey{prefixInbound, inbound}] = newLimiter(download, upload) + } else { + m.mp[limiterKey{prefixInbound, inbound}] = sharedLimiter + } } return } diff --git a/option/limiter.go b/option/limiter.go index 99f1dc629b..a45b6393a9 100644 --- a/option/limiter.go +++ b/option/limiter.go @@ -1,9 +1,11 @@ package option type Limiter struct { - Tag string `json:"tag"` - Download string `json:"download,omitempty"` - Upload string `json:"upload,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Inbound Listable[string] `json:"inbound,omitempty"` + Tag string `json:"tag"` + Download string `json:"download,omitempty"` + Upload string `json:"upload,omitempty"` + AuthUser Listable[string] `json:"auth_user,omitempty"` + AuthUserIndependent bool `json:"auth_user_independent,omitempty"` + Inbound Listable[string] `json:"inbound,omitempty"` + InboundIndependent bool `json:"inbound_independent,omitempty"` } From ad1ed94c16a5552bd306c2798573099c23a774f2 Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:09:50 +0800 Subject: [PATCH 4/6] fix: align down/up bandwidth with user perspective --- docs/configuration/limiter/index.md | 4 ++-- docs/configuration/limiter/index.zh.md | 4 ++-- limiter/limiter.go | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/configuration/limiter/index.md b/docs/configuration/limiter/index.md index 6371d080d2..b0f1ec56c7 100644 --- a/docs/configuration/limiter/index.md +++ b/docs/configuration/limiter/index.md @@ -7,8 +7,8 @@ "limiters": [ { "tag": "limiter-a", - "download": "1M", - "upload": "10M", + "download": "10M", + "upload": "1M", "auth_user": [ "user-a", "user-b" diff --git a/docs/configuration/limiter/index.zh.md b/docs/configuration/limiter/index.zh.md index 5b71cf3de1..342635de22 100644 --- a/docs/configuration/limiter/index.zh.md +++ b/docs/configuration/limiter/index.zh.md @@ -7,8 +7,8 @@ "limiters": [ { "tag": "limiter-a", - "download": "1M", - "upload": "10M", + "download": "10M", + "upload": "1M", "auth_user": [ "user-a", "user-b" diff --git a/limiter/limiter.go b/limiter/limiter.go index 8679c558ae..aba19fa7c3 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -30,10 +30,10 @@ type connWithLimiter struct { } func (conn *connWithLimiter) Read(p []byte) (n int, err error) { - if conn.limiter == nil || conn.limiter.downloadLimiter == nil { + if conn.limiter == nil || conn.limiter.uploadLimiter == nil { return conn.Conn.Read(p) } - b := conn.limiter.downloadLimiter.Burst() + b := conn.limiter.uploadLimiter.Burst() if b < len(p) { p = p[:b] } @@ -41,7 +41,7 @@ func (conn *connWithLimiter) Read(p []byte) (n int, err error) { if err != nil { return } - err = conn.limiter.downloadLimiter.WaitN(conn.ctx, n) + err = conn.limiter.uploadLimiter.WaitN(conn.ctx, n) if err != nil { return } @@ -49,11 +49,11 @@ func (conn *connWithLimiter) Read(p []byte) (n int, err error) { } func (conn *connWithLimiter) Write(p []byte) (n int, err error) { - if conn.limiter == nil || conn.limiter.uploadLimiter == nil { + if conn.limiter == nil || conn.limiter.downloadLimiter == nil { return conn.Conn.Write(p) } var nn int - b := conn.limiter.uploadLimiter.Burst() + b := conn.limiter.downloadLimiter.Burst() for { end := len(p) if end == 0 { @@ -62,7 +62,7 @@ func (conn *connWithLimiter) Write(p []byte) (n int, err error) { if b < len(p) { end = b } - err = conn.limiter.uploadLimiter.WaitN(conn.ctx, end) + err = conn.limiter.downloadLimiter.WaitN(conn.ctx, end) if err != nil { return } From ef6b563d026c6b0bdc177cfea0945a190e79dc7a Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Mon, 21 Aug 2023 10:34:07 +0800 Subject: [PATCH 5/6] fix: doc sidebar --- mkdocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 5632affae6..c7feef09cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,6 +59,8 @@ nav: - FakeIP: configuration/dns/fakeip.md - NTP: - configuration/ntp/index.md + - Limiter: + - configuration/limiter/index.md - Route: - configuration/route/index.md - GeoIP: configuration/route/geoip.md @@ -185,6 +187,8 @@ plugins: DNS Server: DNS 服务器 DNS Rule: DNS 规则 + Limiter: 限速 + Route: 路由 Route Rule: 路由规则 Protocol Sniff: 协议探测 From 630cdb075709496fddfecadf1cc537d761be6096 Mon Sep 17 00:00:00 2001 From: zakuwaki <79925675+zakuwaki@users.noreply.github.com> Date: Wed, 20 Sep 2023 19:36:45 +0800 Subject: [PATCH 6/6] refractor: create limiter conn --- limiter/builder.go | 28 +++++++++++++++------------- limiter/manager.go | 5 +++-- route/router.go | 9 +-------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/limiter/builder.go b/limiter/builder.go index ea16faee42..78e330739d 100644 --- a/limiter/builder.go +++ b/limiter/builder.go @@ -5,6 +5,7 @@ import ( "fmt" "net" + "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/humanize" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -86,22 +87,23 @@ func (m *defaultManager) createLimiter(ctx context.Context, option option.Limite return } -func (m *defaultManager) LoadLimiters(tags []string, user, inbound string) (limiters []*limiter) { - for _, tag := range tags { - if v, ok := m.mp[limiterKey{prefixTag, tag}]; ok { - limiters = append(limiters, v) +func (m *defaultManager) NewConnWithLimiters(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, rule adapter.Rule) net.Conn { + var limiters []*limiter + if rule != nil { + for _, tag := range rule.Limiters() { + if v, ok := m.mp[limiterKey{prefixTag, tag}]; ok { + limiters = append(limiters, v) + } } } - if v, ok := m.mp[limiterKey{prefixUser, user}]; ok { - limiters = append(limiters, v) - } - if v, ok := m.mp[limiterKey{prefixInbound, inbound}]; ok { - limiters = append(limiters, v) + if metadata != nil { + if v, ok := m.mp[limiterKey{prefixUser, metadata.User}]; ok { + limiters = append(limiters, v) + } + if v, ok := m.mp[limiterKey{prefixInbound, metadata.Inbound}]; ok { + limiters = append(limiters, v) + } } - return -} - -func (m *defaultManager) NewConnWithLimiters(ctx context.Context, conn net.Conn, limiters []*limiter) net.Conn { for _, limiter := range limiters { conn = &connWithLimiter{Conn: conn, limiter: limiter, ctx: ctx} } diff --git a/limiter/manager.go b/limiter/manager.go index 4521393074..49531f3589 100644 --- a/limiter/manager.go +++ b/limiter/manager.go @@ -3,9 +3,10 @@ package limiter import ( "context" "net" + + "github.com/sagernet/sing-box/adapter" ) type Manager interface { - LoadLimiters(tags []string, user, inbound string) []*limiter - NewConnWithLimiters(ctx context.Context, conn net.Conn, limiters []*limiter) net.Conn + NewConnWithLimiters(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, rule adapter.Rule) net.Conn } diff --git a/route/router.go b/route/router.go index e2a79fc586..02314e6dd3 100644 --- a/route/router.go +++ b/route/router.go @@ -708,14 +708,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } if r.limiterManager != nil { - var limiterTags []string - if matchedRule != nil { - limiterTags = matchedRule.Limiters() - } - limiters := r.limiterManager.LoadLimiters(limiterTags, metadata.User, metadata.Inbound) - if len(limiters) > 0 { - conn = r.limiterManager.NewConnWithLimiters(ctx, conn, limiters) - } + conn = r.limiterManager.NewConnWithLimiters(ctx, conn, &metadata, matchedRule) } if r.clashServer != nil {