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)
|
||||
NeedWIFIState() bool
|
||||
Rules() []Rule
|
||||
AddRule(rule Rule) int
|
||||
RemoveRule(index int) error
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
12
box.go
12
box.go
@ -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 {
|
||||
|
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
|
||||
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"`
|
||||
}
|
@ -7,6 +7,7 @@ type ExperimentalOptions struct {
|
||||
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 {
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
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 {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user