Merge 630cdb075709496fddfecadf1cc537d761be6096 into c5ba03a2f7bf53c9e3bd555d292be21f47b0e932

This commit is contained in:
zakuwaki 2023-11-16 11:11:44 +08:00 committed by GitHub
commit c89a66edbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 397 additions and 8 deletions

View File

@ -67,6 +67,7 @@ type Rule interface {
Match(metadata *InboundContext) bool Match(metadata *InboundContext) bool
Outbound() string Outbound() string
String() string String() string
Limiters() []string
} }
type DNSRule interface { type DNSRule interface {

4
box.go
View File

@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/inbound"
"github.com/sagernet/sing-box/limiter"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/outbound"
@ -77,6 +78,9 @@ func New(options Options) (*Box, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create log factory") 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( router, err := route.NewRouter(
ctx, ctx,
logFactory, logFactory,

View File

@ -11,6 +11,7 @@ sing-box uses JSON for configuration files.
"ntp": {}, "ntp": {},
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"limiters": [],
"route": {}, "route": {},
"experimental": {} "experimental": {}
} }
@ -25,6 +26,7 @@ sing-box uses JSON for configuration files.
| `ntp` | [NTP](./ntp) | | `ntp` | [NTP](./ntp) |
| `inbounds` | [Inbound](./inbound) | | `inbounds` | [Inbound](./inbound) |
| `outbounds` | [Outbound](./outbound) | | `outbounds` | [Outbound](./outbound) |
| `limiters` | [Limiter](./limiter) |
| `route` | [Route](./route) | | `route` | [Route](./route) |
| `experimental` | [Experimental](./experimental) | | `experimental` | [Experimental](./experimental) |

View File

@ -10,6 +10,7 @@ sing-box 使用 JSON 作为配置文件格式。
"dns": {}, "dns": {},
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"limiters": [],
"route": {}, "route": {},
"experimental": {} "experimental": {}
} }
@ -23,6 +24,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `dns` | [DNS](./dns) | | `dns` | [DNS](./dns) |
| `inbounds` | [入站](./inbound) | | `inbounds` | [入站](./inbound) |
| `outbounds` | [出站](./outbound) | | `outbounds` | [出站](./outbound) |
| `limiters` | [限速](./limiter) |
| `route` | [路由](./route) | | `route` | [路由](./route) |
| `experimental` | [实验性](./experimental) | | `experimental` | [实验性](./experimental) |

View File

@ -0,0 +1,56 @@
# Limiter
### Structure
```json
{
"limiters": [
{
"tag": "limiter-a",
"download": "10M",
"upload": "1M",
"auth_user": [
"user-a",
"user-b"
],
"auth_user_independent": false,
"inbound": [
"in-a",
"in-b"
],
"inbound_independent": false
}
]
}
```
### 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
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
Apply limiter for a group of inbounds.
#### inbound_independent
Make each inbound's limiter independent. If disabled, the same limiter will be shared.

View File

@ -0,0 +1,56 @@
# 限速
### 结构
```json
{
"limiters": [
{
"tag": "limiter-a",
"download": "10M",
"upload": "1M",
"auth_user": [
"user-a",
"user-b"
],
"auth_user_independent": false,
"inbound": [
"in-a",
"in-b"
],
"inbound_independent": false
}
]
}
```
### 字段
#### download upload
==必填==
格式: `[Integer][Unit]` 例如: `100M, 100m, 1G, 1g`.
支持的单位 (大小写不敏感): `B, K, M, G, T, P, E`.
#### tag
限速标签,在路由规则中使用。
#### auth_user
用户组限速,参阅入站设置。
#### auth_user_independent
使每个用户有单独的限速。关闭时将共享限速。
#### inbound
入站组限速。
#### inbound_independent
使每个入站有单独的限速。关闭时将共享限速。

View File

@ -84,14 +84,22 @@
], ],
"clash_mode": "direct", "clash_mode": "direct",
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct",
"limiter": [
"limiter-a",
"limiter-b"
]
}, },
{ {
"type": "logical", "type": "logical",
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct",
"limiter": [
"limiter-a",
"limiter-b"
]
} }
] ]
} }
@ -238,6 +246,10 @@ Invert match result.
Tag of the target outbound. Tag of the target outbound.
#### limiter
Tags of [Limiter](/configuration/limiter). Take effect for all connections matching this rule.
### Logical Fields ### Logical Fields
#### type #### type

View File

@ -82,14 +82,22 @@
], ],
"clash_mode": "direct", "clash_mode": "direct",
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct",
"limiter": [
"limiter-a",
"limiter-b"
]
}, },
{ {
"type": "logical", "type": "logical",
"mode": "and", "mode": "and",
"rules": [], "rules": [],
"invert": false, "invert": false,
"outbound": "direct" "outbound": "direct",
"limiter": [
"limiter-a",
"limiter-b"
]
} }
] ]
} }
@ -236,6 +244,10 @@
目标出站的标签。 目标出站的标签。
#### limiter
[限速](/zh/configuration/inbound) 标签。对所有匹配该规则的连接生效。
### 逻辑字段 ### 逻辑字段
#### type #### type

111
limiter/builder.go Normal file
View File

@ -0,0 +1,111 @@
package limiter
import (
"context"
"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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
)
const (
prefixTag = "tag"
prefixUser = "user"
prefixInbound = "inbound"
)
var _ Manager = (*defaultManager)(nil)
type limiterKey struct {
Prefix string
Name string
}
type defaultManager struct {
mp map[limiterKey]*limiter
}
func WithDefault(ctx context.Context, logger log.ContextLogger, options []option.Limiter) context.Context {
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))
} 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 (m *defaultManager) createLimiter(ctx context.Context, option option.Limiter) (err error) {
var download, upload uint64
if option.Download != "" {
download, err = humanize.ParseBytes(option.Download)
if err != nil {
return err
}
}
if option.Upload != "" {
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")
}
if option.Tag == "" && len(option.AuthUser) == 0 && len(option.Inbound) == 0 {
return E.New("tag/user/inbound, at least one must be set")
}
var sharedLimiter *limiter
if option.Tag != "" || !option.AuthUserIndependent || !option.InboundIndependent {
sharedLimiter = newLimiter(download, upload)
}
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
}
}
for _, inbound := range option.Inbound {
if option.InboundIndependent {
m.mp[limiterKey{prefixInbound, inbound}] = newLimiter(download, upload)
} else {
m.mp[limiterKey{prefixInbound, inbound}] = sharedLimiter
}
}
return
}
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 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)
}
}
for _, limiter := range limiters {
conn = &connWithLimiter{Conn: conn, limiter: limiter, ctx: ctx}
}
return conn
}

77
limiter/limiter.go Normal file
View 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.uploadLimiter == nil {
return conn.Conn.Read(p)
}
b := conn.limiter.uploadLimiter.Burst()
if b < len(p) {
p = p[:b]
}
n, err = conn.Conn.Read(p)
if err != nil {
return
}
err = conn.limiter.uploadLimiter.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.downloadLimiter == nil {
return conn.Conn.Write(p)
}
var nn int
b := conn.limiter.downloadLimiter.Burst()
for {
end := len(p)
if end == 0 {
break
}
if b < len(p) {
end = b
}
err = conn.limiter.downloadLimiter.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
}

12
limiter/manager.go Normal file
View File

@ -0,0 +1,12 @@
package limiter
import (
"context"
"net"
"github.com/sagernet/sing-box/adapter"
)
type Manager interface {
NewConnWithLimiters(ctx context.Context, conn net.Conn, metadata *adapter.InboundContext, rule adapter.Rule) net.Conn
}

View File

@ -59,6 +59,8 @@ nav:
- FakeIP: configuration/dns/fakeip.md - FakeIP: configuration/dns/fakeip.md
- NTP: - NTP:
- configuration/ntp/index.md - configuration/ntp/index.md
- Limiter:
- configuration/limiter/index.md
- Route: - Route:
- configuration/route/index.md - configuration/route/index.md
- GeoIP: configuration/route/geoip.md - GeoIP: configuration/route/geoip.md
@ -186,6 +188,8 @@ plugins:
DNS Server: DNS 服务器 DNS Server: DNS 服务器
DNS Rule: DNS 规则 DNS Rule: DNS 规则
Limiter: 限速
Route: 路由 Route: 路由
Route Rule: 路由规则 Route Rule: 路由规则
Protocol Sniff: 协议探测 Protocol Sniff: 协议探测

View File

@ -16,6 +16,7 @@ type _Options struct {
Inbounds []Inbound `json:"inbounds,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"` Route *RouteOptions `json:"route,omitempty"`
Limiters []Limiter `json:"limiters,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"`
} }

11
option/limiter.go Normal file
View File

@ -0,0 +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"`
AuthUserIndependent bool `json:"auth_user_independent,omitempty"`
Inbound Listable[string] `json:"inbound,omitempty"`
InboundIndependent bool `json:"inbound_independent,omitempty"`
}

View File

@ -80,6 +80,7 @@ type DefaultRule struct {
ClashMode string `json:"clash_mode,omitempty"` ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
Limiter Listable[string] `json:"limiter,omitempty"`
} }
func (r DefaultRule) IsValid() bool { func (r DefaultRule) IsValid() bool {
@ -90,10 +91,11 @@ func (r DefaultRule) IsValid() bool {
} }
type LogicalRule struct { type LogicalRule struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"` Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"` Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
Limiter Listable[string] `json:"limiter,omitempty"`
} }
func (r LogicalRule) IsValid() bool { func (r LogicalRule) IsValid() bool {

View File

@ -20,6 +20,7 @@ import (
"github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/libbox/platform" "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/log"
"github.com/sagernet/sing-box/ntp" "github.com/sagernet/sing-box/ntp"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
@ -85,6 +86,7 @@ type Router struct {
pauseManager pause.Manager pauseManager pause.Manager
clashServer adapter.ClashServer clashServer adapter.ClashServer
v2rayServer adapter.V2RayServer v2rayServer adapter.V2RayServer
limiterManager limiter.Manager
platformInterface platform.Interface platformInterface platform.Interface
} }
@ -498,6 +500,9 @@ func (r *Router) Start() error {
return E.Cause(err, "initialize time service") return E.Cause(err, "initialize time service")
} }
} }
if limiterManger := service.FromContext[limiter.Manager](r.ctx); limiterManger != nil {
r.limiterManager = limiterManger
}
return nil return nil
} }
@ -689,6 +694,11 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
if !common.Contains(detour.Network(), N.NetworkTCP) { if !common.Contains(detour.Network(), N.NetworkTCP) {
return E.New("missing supported outbound, closing connection") return E.New("missing supported outbound, closing connection")
} }
if r.limiterManager != nil {
conn = r.limiterManager.NewConnWithLimiters(ctx, conn, &metadata, matchedRule)
}
if r.clashServer != nil { if r.clashServer != nil {
trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule) trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule)
defer tracker.Leave() defer tracker.Leave()

View File

@ -18,6 +18,7 @@ type abstractDefaultRule struct {
allItems []RuleItem allItems []RuleItem
invert bool invert bool
outbound string outbound string
limiters []string
} }
func (r *abstractDefaultRule) Type() string { func (r *abstractDefaultRule) Type() string {
@ -126,6 +127,10 @@ func (r *abstractDefaultRule) Outbound() string {
return r.outbound return r.outbound
} }
func (r *abstractDefaultRule) Limiters() []string {
return r.limiters
}
func (r *abstractDefaultRule) String() string { func (r *abstractDefaultRule) String() string {
if !r.invert { if !r.invert {
return strings.Join(F.MapToString(r.allItems), " ") return strings.Join(F.MapToString(r.allItems), " ")
@ -139,6 +144,7 @@ type abstractLogicalRule struct {
mode string mode string
invert bool invert bool
outbound string outbound string
limiters []string
} }
func (r *abstractLogicalRule) Type() string { func (r *abstractLogicalRule) Type() string {
@ -191,6 +197,10 @@ func (r *abstractLogicalRule) Outbound() string {
return r.outbound return r.outbound
} }
func (r *abstractLogicalRule) Limiters() []string {
return r.limiters
}
func (r *abstractLogicalRule) String() string { func (r *abstractLogicalRule) String() string {
var op string var op string
switch r.mode { switch r.mode {

View File

@ -184,6 +184,9 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
rule.items = append(rule.items, item) rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item) rule.allItems = append(rule.allItems, item)
} }
if len(options.Limiter) > 0 {
rule.limiters = append(rule.limiters, options.Limiter...)
}
return rule, nil return rule, nil
} }
@ -216,5 +219,8 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
} }
r.rules[i] = rule r.rules[i] = rule
} }
if len(options.Limiter) > 0 {
r.limiters = append(r.limiters, options.Limiter...)
}
return r, nil return r, nil
} }