添加with_dynamic_api

This commit is contained in:
mingtianquan 2025-04-18 20:33:16 +08:00
parent 50384bd0b0
commit ad42f74dcb
21 changed files with 894 additions and 52 deletions

6
adapter/dynamic.go Normal file
View File

@ -0,0 +1,6 @@
package adapter
// DynamicManager 是用于动态管理入站、出站和路由规则的接口
type DynamicManager interface {
LifecycleService
}

View File

@ -24,6 +24,8 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
NeedWIFIState() bool
Rules() []Rule
AddRule(rule Rule) int
RemoveRule(index int) error
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}

12
box.go
View File

@ -116,6 +116,7 @@ func New(options Options) (*Box, error) {
var needCacheFile bool
var needClashAPI bool
var needV2RayAPI bool
var needDynamicAPI bool
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled || options.PlatformLogWriter != nil {
needCacheFile = true
}
@ -125,6 +126,9 @@ func New(options Options) (*Box, error) {
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
if experimentalOptions.DynamicAPI != nil && experimentalOptions.DynamicAPI.Listen != "" {
needDynamicAPI = true
}
platformInterface := service.FromContext[platform.Interface](ctx)
var defaultLogWriter io.Writer
if platformInterface != nil {
@ -329,6 +333,14 @@ func New(options Options) (*Box, error) {
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 {
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
if err != nil {

View 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)
}

View File

@ -0,0 +1,9 @@
package dynamicapi
import (
"github.com/sagernet/sing-box/experimental"
)
func init() {
experimental.RegisterDynamicManagerConstructor(NewServer)
}

View 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
View File

@ -0,0 +1,5 @@
//go:build with_dynamic_api
package include
import _ "github.com/sagernet/sing-box/experimental/dynamicapi"

View 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`)
})
}

View File

@ -387,3 +387,50 @@ type DHCPDNSServerOptions struct {
LocalDNSServerOptions
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
View File

@ -0,0 +1,8 @@
package option
type DynamicAPIOptions struct {
// DynamicAPI服务器监听地址
Listen string `json:"listen"`
// API认证密钥
Secret string `json:"secret"`
}

View File

@ -3,10 +3,11 @@ package option
import "github.com/sagernet/sing/common/json/badoption"
type ExperimentalOptions struct {
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
Debug *DebugOptions `json:"debug,omitempty"`
DynamicAPI *DynamicAPIOptions `json:"dynamic_api,omitempty"`
}
type CacheFileOptions struct {

View File

@ -91,6 +91,7 @@ type RawDefaultRule struct {
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"`

View File

@ -9,7 +9,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/common/json/badoption"
)
type _DNSRule struct {
@ -67,50 +66,7 @@ func (r DNSRule) IsValid() bool {
}
}
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"`
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"`
}
// RawDefaultDNSRule已在dns.go中定义
type DefaultDNSRule struct {
RawDefaultDNSRule

View File

@ -161,6 +161,7 @@ type DefaultHeadlessRule struct {
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"`
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
NetworkIsExpensive bool `json:"network_is_expensive,omitempty"`

View File

@ -203,6 +203,23 @@ func (r *Router) Rules() []adapter.Rule {
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) {
r.trackers = append(r.trackers, tracker)
}

View File

@ -198,6 +198,11 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
rule.items = append(rule.items, 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 {
item := NewPackageNameItem(options.PackageName)
rule.items = append(rule.items, item)

View File

@ -194,6 +194,11 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.items = append(rule.items, 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 {
item := NewPackageNameItem(options.PackageName)
rule.items = append(rule.items, item)

View File

@ -136,6 +136,11 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
rule.items = append(rule.items, 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 {
item := NewPackageNameItem(options.PackageName)
rule.items = append(rule.items, item)

View 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
}

View File

@ -59,7 +59,7 @@ func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultH
}
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 {

View File

@ -38,11 +38,11 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo
}
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 {
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 {