mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
feat: add bandwidth limiter
This commit is contained in:
parent
ae8187ed15
commit
ab1412a0c9
@ -70,6 +70,7 @@ type Rule interface {
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
String() string
|
||||
Limiters() []string
|
||||
}
|
||||
|
||||
type DNSRule interface {
|
||||
|
4
box.go
4
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,
|
||||
|
@ -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) |
|
||||
|
||||
|
@ -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) |
|
||||
|
||||
|
50
docs/configuration/limiter/index.md
Normal file
50
docs/configuration/limiter/index.md
Normal file
@ -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.
|
50
docs/configuration/limiter/index.zh.md
Normal file
50
docs/configuration/limiter/index.zh.md
Normal file
@ -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 ""
|
||||
|
||||
所有用户、入站和有限速标签的路由规则共享同一个限速。为了独立生效,请分别配置限速器。
|
@ -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
|
||||
|
@ -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
|
||||
|
106
limiter/builder.go
Normal file
106
limiter/builder.go
Normal file
@ -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
|
||||
}
|
77
limiter/limiter.go
Normal file
77
limiter/limiter.go
Normal file
@ -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
|
||||
}
|
11
limiter/manager.go
Normal file
11
limiter/manager.go
Normal file
@ -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
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
||||
|
9
option/limiter.go
Normal file
9
option/limiter.go
Normal file
@ -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"`
|
||||
}
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user