Skip to content

Commit

Permalink
feat: add bandwidth limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
zakuwaki committed Jul 15, 2023
1 parent 1a29c23 commit 82dba1f
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 8 deletions.
1 change: 1 addition & 0 deletions adapter/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Rule interface {
Match(metadata *InboundContext) bool
Outbound() string
String() string
Limiters() []string
}

type DNSRule interface {
Expand Down
4 changes: 4 additions & 0 deletions box.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -72,6 +73,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,
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ sing-box uses JSON for configuration files.
"ntp": {},
"inbounds": [],
"outbounds": [],
"limiters": [],
"route": {},
"experimental": {}
}
Expand All @@ -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) |

Expand Down
2 changes: 2 additions & 0 deletions docs/configuration/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
"dns": {},
"inbounds": [],
"outbounds": [],
"limiters": [],
"route": {},
"experimental": {}
}
Expand All @@ -23,6 +24,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `dns` | [DNS](./dns) |
| `inbounds` | [入站](./inbound) |
| `outbounds` | [出站](./outbound) |
| `limiters` | [限速](./limiter) |
| `route` | [路由](./route) |
| `experimental` | [实验性](./experimental) |

Expand Down
50 changes: 50 additions & 0 deletions docs/configuration/limiter/index.md
Original file line number Diff line number Diff line change
@@ -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 changes: 50 additions & 0 deletions docs/configuration/limiter/index.zh.md
Original file line number Diff line number Diff line change
@@ -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 ""

所有用户、入站和有限速标签的路由规则共享同一个限速。为了独立生效,请分别配置限速器。
16 changes: 14 additions & 2 deletions docs/configuration/route/rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions docs/configuration/route/rule.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
Expand Down Expand Up @@ -236,6 +244,10 @@

目标出站的标签。

#### limiter

[限速](/zh/configuration/inbound) 标签。对所有匹配该规则的连接生效。

### 逻辑字段

#### type
Expand Down
106 changes: 106 additions & 0 deletions limiter/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package limiter

import (
"context"
"fmt"
"net"
"sync"

"github.com/dustin/go-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
}
Loading

0 comments on commit 82dba1f

Please sign in to comment.