mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
添加with_dynamic_api
This commit is contained in:
parent
50384bd0b0
commit
ad42f74dcb
6
adapter/dynamic.go
Normal file
6
adapter/dynamic.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
// DynamicManager 是用于动态管理入站、出站和路由规则的接口
|
||||||
|
type DynamicManager interface {
|
||||||
|
LifecycleService
|
||||||
|
}
|
@ -24,6 +24,8 @@ type Router interface {
|
|||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
NeedWIFIState() bool
|
NeedWIFIState() bool
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
AddRule(rule Rule) int
|
||||||
|
RemoveRule(index int) error
|
||||||
AppendTracker(tracker ConnectionTracker)
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
12
box.go
12
box.go
@ -116,6 +116,7 @@ func New(options Options) (*Box, error) {
|
|||||||
var needCacheFile bool
|
var needCacheFile bool
|
||||||
var needClashAPI bool
|
var needClashAPI bool
|
||||||
var needV2RayAPI bool
|
var needV2RayAPI bool
|
||||||
|
var needDynamicAPI bool
|
||||||
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
|
||||||
needCacheFile = true
|
needCacheFile = true
|
||||||
}
|
}
|
||||||
@ -125,6 +126,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
needV2RayAPI = true
|
needV2RayAPI = true
|
||||||
}
|
}
|
||||||
|
if experimentalOptions.DynamicAPI != nil && experimentalOptions.DynamicAPI.Listen != "" {
|
||||||
|
needDynamicAPI = true
|
||||||
|
}
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
var defaultLogWriter io.Writer
|
var defaultLogWriter io.Writer
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
@ -329,6 +333,14 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if needDynamicAPI {
|
||||||
|
dynamicAPIOptions := common.PtrValueOrDefault(experimentalOptions.DynamicAPI)
|
||||||
|
dynamicServer, err := experimental.NewDynamicManager(ctx, logFactory.NewLogger("dynamic-api"), dynamicAPIOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create dynamic-api server")
|
||||||
|
}
|
||||||
|
services = append(services, dynamicServer)
|
||||||
|
}
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
21
experimental/dynamicapi.go
Normal file
21
experimental/dynamicapi.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package experimental
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dynamicManagerConstructor DynamicManagerConstructor
|
||||||
|
|
||||||
|
type DynamicManagerConstructor func(ctx context.Context, logger log.ContextLogger, options option.DynamicAPIOptions) (adapter.DynamicManager, error)
|
||||||
|
|
||||||
|
func RegisterDynamicManagerConstructor(constructor DynamicManagerConstructor) {
|
||||||
|
dynamicManagerConstructor = constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDynamicManager(ctx context.Context, logger log.ContextLogger, options option.DynamicAPIOptions) (adapter.DynamicManager, error) {
|
||||||
|
return dynamicManagerConstructor(ctx, logger, options)
|
||||||
|
}
|
9
experimental/dynamicapi/init.go
Normal file
9
experimental/dynamicapi/init.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package dynamicapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/experimental"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
experimental.RegisterDynamicManagerConstructor(NewServer)
|
||||||
|
}
|
678
experimental/dynamicapi/server.go
Normal file
678
experimental/dynamicapi/server.go
Normal file
@ -0,0 +1,678 @@
|
|||||||
|
package dynamicapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
R "github.com/sagernet/sing-box/route/rule"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.DynamicManager = (*Server)(nil)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
router adapter.Router
|
||||||
|
inbound adapter.InboundManager
|
||||||
|
outbound adapter.OutboundManager
|
||||||
|
logger log.ContextLogger
|
||||||
|
logFactory log.Factory
|
||||||
|
httpServer *http.Server
|
||||||
|
listenAddress string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(ctx context.Context, logger log.ContextLogger, options option.DynamicAPIOptions) (adapter.DynamicManager, error) {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
inboundManager := service.FromContext[adapter.InboundManager](ctx)
|
||||||
|
outboundManager := service.FromContext[adapter.OutboundManager](ctx)
|
||||||
|
routerInstance := service.FromContext[adapter.Router](ctx)
|
||||||
|
logFactory := service.FromContext[log.Factory](ctx)
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
ctx: ctx,
|
||||||
|
router: routerInstance,
|
||||||
|
inbound: inboundManager,
|
||||||
|
outbound: outboundManager,
|
||||||
|
logger: logger,
|
||||||
|
logFactory: logFactory,
|
||||||
|
listenAddress: options.Listen,
|
||||||
|
secret: options.Secret,
|
||||||
|
httpServer: &http.Server{
|
||||||
|
Addr: options.Listen,
|
||||||
|
Handler: r,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(middleware.Recoverer)
|
||||||
|
r.Use(authentication(options.Secret))
|
||||||
|
|
||||||
|
// 添加API路由
|
||||||
|
r.Route("/api", func(r chi.Router) {
|
||||||
|
// 入站API
|
||||||
|
r.Route("/inbound", func(r chi.Router) {
|
||||||
|
r.Post("/", s.createInbound)
|
||||||
|
r.Delete("/{tag}", s.removeInbound)
|
||||||
|
r.Get("/", s.listInbounds)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 出站API
|
||||||
|
r.Route("/outbound", func(r chi.Router) {
|
||||||
|
r.Post("/", s.createOutbound)
|
||||||
|
r.Delete("/{tag}", s.removeOutbound)
|
||||||
|
r.Get("/", s.listOutbounds)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由规则API
|
||||||
|
r.Route("/route", func(r chi.Router) {
|
||||||
|
r.Post("/rule", s.createRouteRule)
|
||||||
|
r.Delete("/rule/{index}", s.removeRouteRule)
|
||||||
|
r.Get("/rules", s.listRouteRules)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Name() string {
|
||||||
|
return "dynamic api server"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStatePostStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", s.listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "listen on ", s.listenAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("dynamic api server listening at ", listener.Addr())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err = s.httpServer.Serve(listener)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.logger.Error("failed to serve: ", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
if s.httpServer != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return s.httpServer.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改createInbound方法
|
||||||
|
func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从请求体中读取原始JSON数据
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法读取请求体: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// 首先尝试解析整个请求
|
||||||
|
var requestMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &requestMap); err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法解析请求: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取tag和type
|
||||||
|
tag, tagExists := requestMap["tag"].(string)
|
||||||
|
inboundType, typeExists := requestMap["type"].(string)
|
||||||
|
|
||||||
|
if !tagExists || !typeExists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "tag和type不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查入站是否已存在
|
||||||
|
if _, exists := s.inbound.Get(tag); exists {
|
||||||
|
render.Status(r, http.StatusConflict)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "入站已存在: " + tag})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取options
|
||||||
|
var optionsRaw interface{}
|
||||||
|
if options, hasOptions := requestMap["options"]; hasOptions {
|
||||||
|
optionsRaw = options
|
||||||
|
} else {
|
||||||
|
// 如果没有options字段,将请求中除了tag和type外的所有字段作为options
|
||||||
|
optionsMap := make(map[string]interface{})
|
||||||
|
for key, value := range requestMap {
|
||||||
|
if key != "tag" && key != "type" {
|
||||||
|
optionsMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optionsRaw = optionsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
s.logger.Info("创建入站: ", inboundType, "[", tag, "]")
|
||||||
|
|
||||||
|
// 获取入站注册表
|
||||||
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](s.ctx)
|
||||||
|
if inboundRegistry == nil {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "入站注册服务不可用"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建入站配置对象
|
||||||
|
optionsObj, exists := inboundRegistry.CreateOptions(inboundType)
|
||||||
|
if !exists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "不支持的入站类型: " + inboundType})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将原始选项转换为正确的结构体
|
||||||
|
optionsJson, err := json.Marshal(optionsRaw)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法序列化选项: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(optionsJson, optionsObj)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "选项格式错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建入站
|
||||||
|
err = s.inbound.Create(s.ctx, s.router, s.logger, tag, inboundType, optionsObj)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "创建入站失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "入站创建成功",
|
||||||
|
"tag": tag,
|
||||||
|
"type": inboundType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除入站
|
||||||
|
func (s *Server) removeInbound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tag := chi.URLParam(r, "tag")
|
||||||
|
if tag == "" {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "tag不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查入站是否存在
|
||||||
|
_, exists := s.inbound.Get(tag)
|
||||||
|
if !exists {
|
||||||
|
render.Status(r, http.StatusNotFound)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "入站不存在: " + tag})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除入站
|
||||||
|
err := s.inbound.Remove(tag)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrInvalid) {
|
||||||
|
render.Status(r, http.StatusNotFound)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "入站不存在: " + tag})
|
||||||
|
} else {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "移除入站失败: " + err.Error()})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "入站移除成功",
|
||||||
|
"tag": tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出所有入站
|
||||||
|
func (s *Server) listInbounds(w http.ResponseWriter, r *http.Request) {
|
||||||
|
inbounds := s.inbound.Inbounds()
|
||||||
|
var result []map[string]string
|
||||||
|
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
result = append(result, map[string]string{
|
||||||
|
"tag": inbound.Tag(),
|
||||||
|
"type": inbound.Type(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"inbounds": result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改createOutbound方法
|
||||||
|
func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从请求体中读取原始JSON数据
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法读取请求体: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// 首先尝试解析整个请求
|
||||||
|
var requestMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &requestMap); err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法解析请求: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取tag和type
|
||||||
|
tag, tagExists := requestMap["tag"].(string)
|
||||||
|
outboundType, typeExists := requestMap["type"].(string)
|
||||||
|
|
||||||
|
if !tagExists || !typeExists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "tag和type不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查出站是否已存在
|
||||||
|
if _, exists := s.outbound.Outbound(tag); exists {
|
||||||
|
render.Status(r, http.StatusConflict)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "出站已存在: " + tag})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取options
|
||||||
|
var optionsRaw interface{}
|
||||||
|
if options, hasOptions := requestMap["options"]; hasOptions {
|
||||||
|
optionsRaw = options
|
||||||
|
} else {
|
||||||
|
// 如果没有options字段,将请求中除了tag和type外的所有字段作为options
|
||||||
|
optionsMap := make(map[string]interface{})
|
||||||
|
for key, value := range requestMap {
|
||||||
|
if key != "tag" && key != "type" {
|
||||||
|
optionsMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
optionsRaw = optionsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
s.logger.Info("创建出站: ", outboundType, "[", tag, "]")
|
||||||
|
|
||||||
|
// 获取出站注册表
|
||||||
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](s.ctx)
|
||||||
|
if outboundRegistry == nil {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "出站注册服务不可用"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建出站配置对象
|
||||||
|
optionsObj, exists := outboundRegistry.CreateOptions(outboundType)
|
||||||
|
if !exists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "不支持的出站类型: " + outboundType})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将原始选项转换为正确的结构体
|
||||||
|
optionsJson, err := json.Marshal(optionsRaw)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法序列化选项: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(optionsJson, optionsObj)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "选项格式错误: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建出站
|
||||||
|
err = s.outbound.Create(s.ctx, s.router, s.logger, tag, outboundType, optionsObj)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "创建出站失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "出站创建成功",
|
||||||
|
"tag": tag,
|
||||||
|
"type": outboundType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除出站
|
||||||
|
func (s *Server) removeOutbound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tag := chi.URLParam(r, "tag")
|
||||||
|
if tag == "" {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "tag不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查出站是否存在
|
||||||
|
_, exists := s.outbound.Outbound(tag)
|
||||||
|
if !exists {
|
||||||
|
render.Status(r, http.StatusNotFound)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "出站不存在: " + tag})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除出站
|
||||||
|
err := s.outbound.Remove(tag)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrInvalid) {
|
||||||
|
render.Status(r, http.StatusNotFound)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "出站不存在: " + tag})
|
||||||
|
} else {
|
||||||
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "移除出站失败: " + err.Error()})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "出站移除成功",
|
||||||
|
"tag": tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出所有出站
|
||||||
|
func (s *Server) listOutbounds(w http.ResponseWriter, r *http.Request) {
|
||||||
|
outbounds := s.outbound.Outbounds()
|
||||||
|
var result []map[string]string
|
||||||
|
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
result = append(result, map[string]string{
|
||||||
|
"tag": outbound.Tag(),
|
||||||
|
"type": outbound.Type(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"outbounds": result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 认证中间件
|
||||||
|
func authentication(serverSecret string) func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if serverSecret == "" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if secret := r.Header.Get("Authorization"); secret != serverSecret {
|
||||||
|
render.Status(r, http.StatusUnauthorized)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "未经授权的访问"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建路由规则
|
||||||
|
func (s *Server) createRouteRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从请求体中读取原始JSON数据
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法读取请求体: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// 首先尝试解析整个请求
|
||||||
|
var requestMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &requestMap); err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "无法解析请求: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查outbound字段,这是必需的
|
||||||
|
outboundRaw, hasOutbound := requestMap["outbound"]
|
||||||
|
if !hasOutbound {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "outbound字段是必需的"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保outbound是字符串
|
||||||
|
outbound, ok := outboundRaw.(string)
|
||||||
|
if !ok {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "outbound必须是字符串"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证outbound标签存在
|
||||||
|
if _, exists := s.outbound.Outbound(outbound); !exists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "指定的出站不存在: " + outbound})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理inbounds字段,如果未提供则使用所有入站
|
||||||
|
var inbounds []string
|
||||||
|
inboundsRaw, hasInbounds := requestMap["inbounds"]
|
||||||
|
|
||||||
|
if hasInbounds {
|
||||||
|
// 确保inbounds是字符串数组
|
||||||
|
switch v := inboundsRaw.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for _, item := range v {
|
||||||
|
if strItem, ok := item.(string); ok {
|
||||||
|
inbounds = append(inbounds, strItem)
|
||||||
|
} else {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "inbounds必须是字符串数组"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "inbounds必须是数组"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证所有inbound标签都存在
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if _, exists := s.inbound.Get(inbound); !exists {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "指定的入站不存在: " + inbound})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有提供inbounds,使用所有现有的入站
|
||||||
|
for _, inb := range s.inbound.Inbounds() {
|
||||||
|
inbounds = append(inbounds, inb.Tag())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何入站,返回错误
|
||||||
|
if len(inbounds) == 0 {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "系统中没有可用的入站,请先创建入站或在请求中指定inbounds"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录日志
|
||||||
|
s.logger.Info("添加路由规则: 从 ", strings.Join(inbounds, ", "), " 到 ", outbound)
|
||||||
|
|
||||||
|
// 准备rule对象
|
||||||
|
rule := option.Rule{
|
||||||
|
Type: "", // 默认为 "default" 类型规则
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
RawDefaultRule: option.RawDefaultRule{
|
||||||
|
Inbound: inbounds,
|
||||||
|
},
|
||||||
|
RuleAction: option.RuleAction{
|
||||||
|
Action: "route", // 设置为route动作
|
||||||
|
RouteOptions: option.RouteActionOptions{
|
||||||
|
Outbound: outbound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从请求中提取其他规则选项
|
||||||
|
if processNameRaw, ok := requestMap["process_name"]; ok {
|
||||||
|
if processNames, ok := processNameRaw.([]interface{}); ok {
|
||||||
|
var processNameList []string
|
||||||
|
for _, pn := range processNames {
|
||||||
|
if pnStr, ok := pn.(string); ok {
|
||||||
|
processNameList = append(processNameList, pnStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(processNameList) > 0 {
|
||||||
|
rule.DefaultOptions.RawDefaultRule.ProcessName = processNameList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加对process_pid的处理
|
||||||
|
if processPIDRaw, ok := requestMap["process_pid"]; ok {
|
||||||
|
if processPIDs, ok := processPIDRaw.([]interface{}); ok {
|
||||||
|
var processPIDList []uint32
|
||||||
|
for _, pid := range processPIDs {
|
||||||
|
if pidFloat, ok := pid.(float64); ok {
|
||||||
|
processPIDList = append(processPIDList, uint32(pidFloat))
|
||||||
|
} else if pidNumber, ok := pid.(json.Number); ok {
|
||||||
|
if pidInt64, err := pidNumber.Int64(); err == nil {
|
||||||
|
processPIDList = append(processPIDList, uint32(pidInt64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(processPIDList) > 0 {
|
||||||
|
rule.DefaultOptions.RawDefaultRule.ProcessPID = processPIDList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建适配器Rule对象并添加到路由系统
|
||||||
|
adapterRule, err := R.NewRule(s.ctx, s.logger, rule, true)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "创建规则失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Router的AddRule方法添加规则
|
||||||
|
ruleIndex := s.router.AddRule(adapterRule)
|
||||||
|
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "路由规则添加成功",
|
||||||
|
"inbounds": inbounds,
|
||||||
|
"outbound": outbound,
|
||||||
|
"index": ruleIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加removeRouteRule方法
|
||||||
|
func (s *Server) removeRouteRule(w http.ResponseWriter, r *http.Request) {
|
||||||
|
indexStr := chi.URLParam(r, "index")
|
||||||
|
if indexStr == "" {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "索引不能为空"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := strconv.Atoi(indexStr)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "索引必须是有效的整数"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Router的RemoveRule方法移除规则
|
||||||
|
err = s.router.RemoveRule(index)
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, map[string]string{"error": "移除规则失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "路由规则已删除",
|
||||||
|
"index": index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加listRouteRules方法
|
||||||
|
func (s *Server) listRouteRules(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 获取实际路由规则列表
|
||||||
|
rawRules := s.router.Rules()
|
||||||
|
|
||||||
|
var rules []map[string]interface{}
|
||||||
|
for i, rule := range rawRules {
|
||||||
|
ruleMap := map[string]interface{}{
|
||||||
|
"index": i,
|
||||||
|
"type": rule.Type(),
|
||||||
|
"outbound": rule.Action().String(),
|
||||||
|
"desc": rule.String(),
|
||||||
|
}
|
||||||
|
rules = append(rules, ruleMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
render.JSON(w, r, map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"rules": rules,
|
||||||
|
})
|
||||||
|
}
|
5
include/dynamicapi.go
Normal file
5
include/dynamicapi.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
//go:build with_dynamic_api
|
||||||
|
|
||||||
|
package include
|
||||||
|
|
||||||
|
import _ "github.com/sagernet/sing-box/experimental/dynamicapi"
|
19
include/dynamicapi_stub.go
Normal file
19
include/dynamicapi_stub.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//go:build !with_dynamic_api
|
||||||
|
|
||||||
|
package include
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/experimental"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
experimental.RegisterDynamicManagerConstructor(func(ctx context.Context, logger log.ContextLogger, options option.DynamicAPIOptions) (adapter.DynamicManager, error) {
|
||||||
|
return nil, E.New(`dynamic api is not included in this build, rebuild with -tags with_dynamic_api`)
|
||||||
|
})
|
||||||
|
}
|
@ -387,3 +387,50 @@ type DHCPDNSServerOptions struct {
|
|||||||
LocalDNSServerOptions
|
LocalDNSServerOptions
|
||||||
Interface string `json:"interface,omitempty"`
|
Interface string `json:"interface,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawDefaultDNSRule struct {
|
||||||
|
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
||||||
|
IPVersion int `json:"ip_version,omitempty"`
|
||||||
|
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
||||||
|
Network badoption.Listable[string] `json:"network,omitempty"`
|
||||||
|
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
||||||
|
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
||||||
|
Client badoption.Listable[string] `json:"client,omitempty"`
|
||||||
|
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||||
|
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||||
|
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||||
|
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||||
|
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||||
|
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||||
|
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||||
|
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||||
|
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||||
|
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||||
|
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||||
|
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
||||||
|
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||||
|
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||||
|
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||||
|
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||||
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
ProcessPID badoption.Listable[uint32] `json:"process_pid,omitempty"`
|
||||||
|
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||||
|
User badoption.Listable[string] `json:"user,omitempty"`
|
||||||
|
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||||
|
Outbound badoption.Listable[string] `json:"outbound,omitempty"`
|
||||||
|
ClashMode string `json:"clash_mode,omitempty"`
|
||||||
|
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||||
|
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||||
|
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
||||||
|
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
||||||
|
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
||||||
|
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||||
|
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||||
|
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||||
|
Invert bool `json:"invert,omitempty"`
|
||||||
|
|
||||||
|
// Deprecated: renamed to rule_set_ip_cidr_match_source
|
||||||
|
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||||
|
}
|
||||||
|
8
option/dynamic.go
Normal file
8
option/dynamic.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
type DynamicAPIOptions struct {
|
||||||
|
// DynamicAPI服务器监听地址
|
||||||
|
Listen string `json:"listen"`
|
||||||
|
// API认证密钥
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
@ -3,10 +3,11 @@ package option
|
|||||||
import "github.com/sagernet/sing/common/json/badoption"
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
type ExperimentalOptions struct {
|
type ExperimentalOptions struct {
|
||||||
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
|
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
|
||||||
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
||||||
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
|
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
|
||||||
Debug *DebugOptions `json:"debug,omitempty"`
|
Debug *DebugOptions `json:"debug,omitempty"`
|
||||||
|
DynamicAPI *DynamicAPIOptions `json:"dynamic_api,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheFileOptions struct {
|
type CacheFileOptions struct {
|
||||||
|
@ -91,6 +91,7 @@ type RawDefaultRule struct {
|
|||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
ProcessPID badoption.Listable[uint32] `json:"process_pid,omitempty"`
|
||||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||||
User badoption.Listable[string] `json:"user,omitempty"`
|
User badoption.Listable[string] `json:"user,omitempty"`
|
||||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
"github.com/sagernet/sing/common/json/badjson"
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type _DNSRule struct {
|
type _DNSRule struct {
|
||||||
@ -67,50 +66,7 @@ func (r DNSRule) IsValid() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawDefaultDNSRule struct {
|
// RawDefaultDNSRule已在dns.go中定义
|
||||||
Inbound badoption.Listable[string] `json:"inbound,omitempty"`
|
|
||||||
IPVersion int `json:"ip_version,omitempty"`
|
|
||||||
QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"`
|
|
||||||
Network badoption.Listable[string] `json:"network,omitempty"`
|
|
||||||
AuthUser badoption.Listable[string] `json:"auth_user,omitempty"`
|
|
||||||
Protocol badoption.Listable[string] `json:"protocol,omitempty"`
|
|
||||||
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
|
||||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
|
||||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
|
||||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
|
||||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
|
||||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
|
||||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
|
||||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
|
||||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
|
||||||
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
|
||||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
|
||||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
|
||||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
|
||||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
|
||||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
|
||||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
|
||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
|
||||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
|
||||||
User badoption.Listable[string] `json:"user,omitempty"`
|
|
||||||
UserID badoption.Listable[int32] `json:"user_id,omitempty"`
|
|
||||||
Outbound badoption.Listable[string] `json:"outbound,omitempty"`
|
|
||||||
ClashMode string `json:"clash_mode,omitempty"`
|
|
||||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
|
||||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
|
||||||
NetworkIsConstrained bool `json:"network_is_constrained,omitempty"`
|
|
||||||
WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"`
|
|
||||||
WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"`
|
|
||||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
|
||||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
|
||||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
|
||||||
Invert bool `json:"invert,omitempty"`
|
|
||||||
|
|
||||||
// Deprecated: renamed to rule_set_ip_cidr_match_source
|
|
||||||
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DefaultDNSRule struct {
|
type DefaultDNSRule struct {
|
||||||
RawDefaultDNSRule
|
RawDefaultDNSRule
|
||||||
|
@ -161,6 +161,7 @@ type DefaultHeadlessRule struct {
|
|||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
ProcessPID badoption.Listable[uint32] `json:"process_pid,omitempty"`
|
||||||
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
PackageName badoption.Listable[string] `json:"package_name,omitempty"`
|
||||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||||
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`
|
||||||
|
@ -203,6 +203,23 @@ func (r *Router) Rules() []adapter.Rule {
|
|||||||
return r.rules
|
return r.rules
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRule 添加新的路由规则
|
||||||
|
func (r *Router) AddRule(rule adapter.Rule) int {
|
||||||
|
r.rules = append(r.rules, rule)
|
||||||
|
return len(r.rules) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRule 根据索引移除路由规则
|
||||||
|
func (r *Router) RemoveRule(index int) error {
|
||||||
|
if index < 0 || index >= len(r.rules) {
|
||||||
|
return E.New("规则索引超出范围")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除指定索引的规则
|
||||||
|
r.rules = append(r.rules[:index], r.rules[index+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) {
|
func (r *Router) AppendTracker(tracker adapter.ConnectionTracker) {
|
||||||
r.trackers = append(r.trackers, tracker)
|
r.trackers = append(r.trackers, tracker)
|
||||||
}
|
}
|
||||||
|
@ -198,6 +198,11 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
|||||||
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.ProcessPID) > 0 {
|
||||||
|
item := NewProcessPIDItem(options.ProcessPID)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
if len(options.PackageName) > 0 {
|
if len(options.PackageName) > 0 {
|
||||||
item := NewPackageNameItem(options.PackageName)
|
item := NewPackageNameItem(options.PackageName)
|
||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
|
@ -194,6 +194,11 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
|||||||
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.ProcessPID) > 0 {
|
||||||
|
item := NewProcessPIDItem(options.ProcessPID)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
if len(options.PackageName) > 0 {
|
if len(options.PackageName) > 0 {
|
||||||
item := NewPackageNameItem(options.PackageName)
|
item := NewPackageNameItem(options.PackageName)
|
||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
|
@ -136,6 +136,11 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
|
|||||||
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.ProcessPID) > 0 {
|
||||||
|
item := NewProcessPIDItem(options.ProcessPID)
|
||||||
|
rule.items = append(rule.items, item)
|
||||||
|
rule.allItems = append(rule.allItems, item)
|
||||||
|
}
|
||||||
if len(options.PackageName) > 0 {
|
if len(options.PackageName) > 0 {
|
||||||
item := NewPackageNameItem(options.PackageName)
|
item := NewPackageNameItem(options.PackageName)
|
||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
|
44
route/rule/rule_item_process_pid.go
Normal file
44
route/rule/rule_item_process_pid.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ RuleItem = (*ProcessPIDItem)(nil)
|
||||||
|
|
||||||
|
type ProcessPIDItem struct {
|
||||||
|
processPIDs []uint32
|
||||||
|
processPIDMap map[uint32]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProcessPIDItem(processPIDList []uint32) *ProcessPIDItem {
|
||||||
|
rule := &ProcessPIDItem{
|
||||||
|
processPIDs: processPIDList,
|
||||||
|
processPIDMap: make(map[uint32]bool),
|
||||||
|
}
|
||||||
|
for _, processPID := range processPIDList {
|
||||||
|
rule.processPIDMap[processPID] = true
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProcessPIDItem) Match(metadata *adapter.InboundContext) bool {
|
||||||
|
if metadata.ProcessInfo == nil || metadata.ProcessInfo.ProcessID == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return r.processPIDMap[metadata.ProcessInfo.ProcessID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProcessPIDItem) String() string {
|
||||||
|
var description string
|
||||||
|
pLen := len(r.processPIDs)
|
||||||
|
if pLen == 1 {
|
||||||
|
description = "process_pid=" + F.ToString(r.processPIDs[0])
|
||||||
|
} else {
|
||||||
|
description = "process_pid=[" + strings.Join(F.MapToString(r.processPIDs), " ") + "]"
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
@ -59,7 +59,7 @@ func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0
|
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.ProcessPID) > 0 || len(rule.PackageName) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||||
|
@ -38,11 +38,11 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isProcessRule(rule option.DefaultRule) bool {
|
func isProcessRule(rule option.DefaultRule) bool {
|
||||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.ProcessPID) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
|
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
|
||||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.ProcessPID) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func isWIFIRule(rule option.DefaultRule) bool {
|
func isWIFIRule(rule option.DefaultRule) bool {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user