mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Merge 630cdb075709496fddfecadf1cc537d761be6096 into c5ba03a2f7bf53c9e3bd555d292be21f47b0e932
This commit is contained in:
commit
c89a66edbf
@ -67,6 +67,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"
|
||||
@ -77,6 +78,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) |
|
||||
|
||||
|
56
docs/configuration/limiter/index.md
Normal file
56
docs/configuration/limiter/index.md
Normal 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.
|
56
docs/configuration/limiter/index.zh.md
Normal file
56
docs/configuration/limiter/index.zh.md
Normal 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
|
||||
|
||||
使每个入站有单独的限速。关闭时将共享限速。
|
@ -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
|
||||
|
111
limiter/builder.go
Normal file
111
limiter/builder.go
Normal 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
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.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
12
limiter/manager.go
Normal 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
|
||||
}
|
@ -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
|
||||
@ -186,6 +188,8 @@ plugins:
|
||||
DNS Server: DNS 服务器
|
||||
DNS Rule: DNS 规则
|
||||
|
||||
Limiter: 限速
|
||||
|
||||
Route: 路由
|
||||
Route Rule: 路由规则
|
||||
Protocol Sniff: 协议探测
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
11
option/limiter.go
Normal file
11
option/limiter.go
Normal 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"`
|
||||
}
|
@ -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 {
|
||||
@ -94,6 +95,7 @@ type LogicalRule struct {
|
||||
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 {
|
||||
|
@ -20,6 +20,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
|
||||
}
|
||||
|
||||
@ -689,6 +694,11 @@ 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 {
|
||||
conn = r.limiterManager.NewConnWithLimiters(ctx, conn, &metadata, matchedRule)
|
||||
}
|
||||
|
||||
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