From 47cba9556698f01aa7a7dab024bc81cf21d00b6d Mon Sep 17 00:00:00 2001 From: mingtianquan Date: Sun, 20 Apr 2025 23:11:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=9D=E5=AD=98=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=92=8C=E9=87=8D=E8=BD=BD=E9=85=8D=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=87=BA=E7=AB=99=E8=BF=9E=E6=8E=A5api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 219 ------------ docs/dynamic-api.md | 240 ++++++++++++++ experimental/dynamicapi/server.go | 534 ++++++++++++++++++++++++++++-- option/dynamic.go | 4 + 4 files changed, 747 insertions(+), 250 deletions(-) create mode 100644 docs/dynamic-api.md diff --git a/README.md b/README.md index 31bc5fca..5de4a65d 100644 --- a/README.md +++ b/README.md @@ -8,225 +8,6 @@ The universal proxy platform. https://sing-box.sagernet.org -## Dynamic API - -使用 Dynamic API 功能,你可以在运行时动态管理入站、出站和路由规则。 - -### 编译时启用 Dynamic API - -在编译时添加 `with_dynamic_api` 标签: - -```bash -go build -tags "with_dynamic_api" ./cmd/sing-box -``` - -或者修改 `Makefile` 中的 `TAGS` 变量: - -``` -TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_reality_server,with_clash_api,with_quic,with_utls,with_tailscale,with_dynamic_api -``` - -### 配置文件示例 - -在配置文件的 `experimental` 部分添加: - -```json -{ - "log": { - "level": "info", - "timestamp": true - }, - "inbounds": [ - { - "type": "http", - "tag": "http-in", - "listen_port": 20808, - "listen": "127.0.0.1" - }, - { - "type": "tun", - "tag": "tun-in", - "interface_name": "sing-box", - "inet4_address": "172.19.0.1/30", - "auto_route": true, - "stack": "system", - "sniff": true - } - ], - "outbounds": [ - { - "type": "direct", - "tag": "direct" - }, - { - "type": "block", - "tag": "block" - } - ], - "route": { - "auto_detect_interface": true, - "rules": [ - { - "action": "sniff" - }, - { - "protocol": "dns", - "action": "hijack-dns" - } - ], - "find_process": true, - "final": "direct" - }, - "experimental": { - "dynamic_api": { - "listen": "127.0.0.1:9090", - "secret": "your_secret_key" - } - } -} -``` - -### API 用法 - -Dynamic API 提供以下功能: - -- 动态添加/删除入站 -- 动态添加/删除出站 -- 动态添加/删除路由规则 - -详细 API 文档请参考官方文档。 - -### 使用示例 - -#### 动态添加入站 - -```bash -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/inbound \ - -d '{ - "tag": "http_in", - "type": "http", - "listen": "127.0.0.1", - "listen_port": 10080, - "users": [ - { - "username": "user", - "password": "pass" - } - ] - }' -``` - -#### 删除入站 - -```bash -curl -X DELETE -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/inbound/http_in -``` - -#### 列出所有入站 - -```bash -curl -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/inbound -``` - -#### 动态添加出站 - -```bash -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/outbound \ - -d '{ - "tag": "proxy_out", - "type": "vmess", - "server": "example.com", - "server_port": 443, - "security": "auto", - "uuid": "bf000d23-0752-40b4-affe-68f7707a9661", - "alter_id": 0, - "tls": { - "enabled": true, - "server_name": "example.com" - } - }' -# -{ - "type": "vless", - "tag": "proxy1", - "server": "shen.86782889.xyz", - "server_port": 55536, - "uuid": "1ecd415e-6b5a-5988-c66f-2e67f28d1e72", - "flow": "", - "network": "tcp", - "tls": { - "enabled": true, - "server_name": "shen.86782889.xyz", - "insecure": false - }, - "transport": { - "type": "ws", - "path": "/SHDsdfsjk2365" - } -} -# - -``` - -#### 删除出站 - -```bash -curl -X DELETE -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/outbound/proxy_out -``` - -#### 动态添加路由规则 - -```bash -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/route/rule \ - -d '{ - "domain": ["example.com"], - "outbound": "proxy_out" - }' -``` - -也可以根据进程名添加规则: - -```bash -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/route/rule \ - -d '{ - "process_name": ["ip2.exe"], - "outbound": "socks-out1" - }' -``` - -或根据进程PID添加规则: - -```bash -curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/route/rule \ - -d '{ - "process_pid": [13588], - "outbound": "proxy1" - }' -``` - -#### 删除路由规则 - -路由规则通过索引删除,首先可以列出所有规则: - -```bash -curl -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/route/rules -``` - -然后删除指定索引的规则(例如删除索引为0的规则): - -```bash -curl -X DELETE -H "Authorization: Bearer your_api_secret" \ - http://127.0.0.1:9090/api/route/rule/0 -``` ## License diff --git a/docs/dynamic-api.md b/docs/dynamic-api.md new file mode 100644 index 00000000..f9be14bf --- /dev/null +++ b/docs/dynamic-api.md @@ -0,0 +1,240 @@ +# Dynamic API 使用指南 + +## 配置说明 + +在 `config.json` 中启用 Dynamic API: + +```json +{ + "log": { + "level": "info", + "timestamp": true + }, + "inbounds": [ + { + "type": "tun", + "tag": "tun-in", + "interface_name": "sing-box", + "inet4_address": "172.19.0.1/30", + "auto_route": true, + "stack": "system", + "sniff": true + } + ], + "outbounds": [ + { + "type": "direct", + "tag": "direct" + }, + { + "type": "block", + "tag": "block" + } + ], + "route": { + "auto_detect_interface": true, + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ], + "find_process": true, + "final": "direct" + }, + "experimental": { + "dynamic_api": { + "listen": "127.0.0.1:9091", + "secret": "your_secret_key111111", + "enable_config_save": true, + "config_save_path": "dynamic_config.json" + } + } +} +``` + +参数说明: +- `listen`: API 服务监听地址 +- `secret`: API 访问密钥,用于认证 +- `enable_config_save`: 是否启用配置保存功能 +- `config_save_path`: 动态配置保存路径 + +## API 端点 + +### 入站管理 + +#### 创建入站 +```bash +curl -X POST http://127.0.0.1:9091/api/inbound \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "http", + "tag": "http-in", + "listen": "127.0.0.1", + "listen_port": 1080 + }' +``` + +#### 删除入站 +```bash +curl -X DELETE http://127.0.0.1:9091/api/inbound/http-in \ + -H "Authorization: your_secret_key" +``` + +#### 列出所有入站 +```bash +curl http://127.0.0.1:9091/api/inbound \ + -H "Authorization: your_secret_key" +``` + +### 出站管理 + +#### 创建出站 +```bash +# VLESS 出站示例 +curl -X POST http://127.0.0.1:9091/api/outbound \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "vless", + "tag": "proxy1", + "server": "example.com", + "server_port": 443, + "uuid": "1234567890-abcd-efgh-ijkl", + "flow": "", + "network": "tcp", + "tls": { + "enabled": true, + "server_name": "example.com", + "insecure": false + }, + "transport": { + "type": "ws", + "path": "/path" + } + }' + +# Shadowsocks 出站示例 +curl -X POST http://127.0.0.1:9091/api/outbound \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "shadowsocks", + "tag": "ss-out", + "server": "example.com", + "server_port": 8388, + "method": "aes-256-gcm", + "password": "your_password" + }' +``` + +#### 删除出站 +```bash +curl -X DELETE http://127.0.0.1:9091/api/outbound/proxy1 \ + -H "Authorization: your_secret_key" +``` + +#### 列出所有出站 +```bash +curl http://127.0.0.1:9091/api/outbound \ + -H "Authorization: your_secret_key" +``` + +#### 测试出站连接 +```bash +curl -X POST http://127.0.0.1:9091/api/outbound/test \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "tag": "proxy1", + "test_url": "http://www.gstatic.com/generate_204" + }' +``` + +### 路由规则管理 + +#### 创建路由规则 +```bash +# 进程规则示例 +curl -X POST http://127.0.0.1:9091/api/route/rule \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "process_name": ["chrome.exe"], + "outbound": "proxy1" + }' + +# 域名规则示例 +curl -X POST http://127.0.0.1:9091/api/route/rule \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "domain": ["example.com", "*.example.com"], + "outbound": "proxy1" + }' + +# IP 规则示例 +curl -X POST http://127.0.0.1:9091/api/route/rule \ + -H "Authorization: your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "ip_cidr": ["192.168.1.0/24", "10.0.0.0/8"], + "outbound": "proxy1" + }' +``` + +#### 删除路由规则 +```bash +curl -X DELETE http://127.0.0.1:9091/api/route/rule/0 \ + -H "Authorization: your_secret_key" +``` + +#### 列出所有规则 +```bash +curl http://127.0.0.1:9091/api/route/rules \ + -H "Authorization: your_secret_key" +``` + +### 配置管理 + +#### 保存配置 +```bash +curl -X POST http://127.0.0.1:9091/api/config/save \ + -H "Authorization: your_secret_key" +``` + +#### 重载配置 +```bash +curl -X POST http://127.0.0.1:9091/api/config/reload \ + -H "Authorization: your_secret_key" +``` + +## 响应格式 + +### 成功响应 +```json +{ + "success": true, + "message": "操作成功", + "data": {} // 可选,包含返回数据 +} +``` + +### 错误响应 +```json +{ + "error": "错误信息" +} +``` + +## 注意事项 + +1. 所有请求都需要在 Header 中包含 `Authorization` 字段,值为配置中的 `secret` +2. 删除操作会同步更新到配置文件(如果启用了配置保存功能) +3. 初始配置(来自 config.json)中的入站和出站不会被保存到动态配置文件中 +4. 系统入站(如 tun)不会被包含在动态配置中 +5. 配置保存功能需要在配置中明确启用(`enable_config_save: true`) \ No newline at end of file diff --git a/experimental/dynamicapi/server.go b/experimental/dynamicapi/server.go index 7038d073..705f7c4f 100644 --- a/experimental/dynamicapi/server.go +++ b/experimental/dynamicapi/server.go @@ -2,11 +2,14 @@ package dynamicapi import ( "context" + "crypto/tls" "encoding/json" "errors" "io" "net" "net/http" + "net/netip" + "net/url" "os" "strconv" "strings" @@ -17,6 +20,7 @@ import ( "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/common/metadata" "github.com/sagernet/sing/service" "github.com/go-chi/chi/v5" @@ -27,15 +31,36 @@ import ( 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 + 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 + enableConfigSave bool + configSavePath string + dynamicConfig *DynamicConfig + inboundConfigs map[string]map[string]interface{} + outboundConfigs map[string]map[string]interface{} + ruleConfigs []map[string]interface{} + initialInbounds map[string]bool + initialOutbounds map[string]bool +} + +// DynamicConfig 结构用于保存动态配置 +type DynamicConfig struct { + Inbounds []map[string]interface{} `json:"inbounds,omitempty"` + Outbounds []map[string]interface{} `json:"outbounds,omitempty"` + Rules []map[string]interface{} `json:"rules,omitempty"` +} + +// TestOutboundRequest 结构用于测试出站请求 +type TestOutboundRequest struct { + Tag string `json:"tag"` + TestURL string `json:"test_url"` } func NewServer(ctx context.Context, logger log.ContextLogger, options option.DynamicAPIOptions) (adapter.DynamicManager, error) { @@ -59,6 +84,21 @@ func NewServer(ctx context.Context, logger log.ContextLogger, options option.Dyn Addr: options.Listen, Handler: r, }, + enableConfigSave: options.EnableConfigSave, + configSavePath: options.ConfigSavePath, + inboundConfigs: make(map[string]map[string]interface{}), + outboundConfigs: make(map[string]map[string]interface{}), + ruleConfigs: make([]map[string]interface{}, 0), + initialInbounds: make(map[string]bool), + initialOutbounds: make(map[string]bool), + } + + // 记录初始配置 + for _, inbound := range inboundManager.Inbounds() { + s.initialInbounds[inbound.Tag()] = true + } + for _, outbound := range outboundManager.Outbounds() { + s.initialOutbounds[outbound.Tag()] = true } r.Use(middleware.Logger) @@ -79,6 +119,7 @@ func NewServer(ctx context.Context, logger log.ContextLogger, options option.Dyn r.Post("/", s.createOutbound) r.Delete("/{tag}", s.removeOutbound) r.Get("/", s.listOutbounds) + r.Post("/test", s.handleTestOutbound) }) // 路由规则API @@ -87,6 +128,12 @@ func NewServer(ctx context.Context, logger log.ContextLogger, options option.Dyn r.Delete("/rule/{index}", s.removeRouteRule) r.Get("/rules", s.listRouteRules) }) + + // 配置管理API + r.Route("/config", func(r chi.Router) { + r.Post("/save", s.handleSaveConfig) + r.Post("/reload", s.handleReloadConfig) + }) }) return s, nil @@ -146,16 +193,16 @@ func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) { return } - // 提取tag和type - tag, tagExists := requestMap["tag"].(string) - inboundType, typeExists := requestMap["type"].(string) - - if !tagExists || !typeExists { + tag, ok := requestMap["tag"].(string) + if !ok { render.Status(r, http.StatusBadRequest) - render.JSON(w, r, map[string]string{"error": "tag和type不能为空"}) + render.JSON(w, r, map[string]string{"error": "tag不能为空"}) return } + // 保存原始配置 + s.inboundConfigs[tag] = requestMap + // 检查入站是否已存在 if _, exists := s.inbound.Get(tag); exists { render.Status(r, http.StatusConflict) @@ -179,7 +226,7 @@ func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) { } // 记录日志 - s.logger.Info("创建入站: ", inboundType, "[", tag, "]") + s.logger.Info("创建入站: ", requestMap["type"], "[", tag, "]") // 获取入站注册表 inboundRegistry := service.FromContext[adapter.InboundRegistry](s.ctx) @@ -190,10 +237,10 @@ func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) { } // 创建入站配置对象 - optionsObj, exists := inboundRegistry.CreateOptions(inboundType) + optionsObj, exists := inboundRegistry.CreateOptions(requestMap["type"].(string)) if !exists { render.Status(r, http.StatusBadRequest) - render.JSON(w, r, map[string]string{"error": "不支持的入站类型: " + inboundType}) + render.JSON(w, r, map[string]string{"error": "不支持的入站类型: " + requestMap["type"].(string)}) return } @@ -213,19 +260,26 @@ func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) { } // 创建入站 - err = s.inbound.Create(s.ctx, s.router, s.logger, tag, inboundType, optionsObj) + err = s.inbound.Create(s.ctx, s.router, s.logger, tag, requestMap["type"].(string), optionsObj) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, map[string]string{"error": "创建入站失败: " + err.Error()}) return } + // 在成功创建入站后保存配置 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.Status(r, http.StatusOK) render.JSON(w, r, map[string]interface{}{ "success": true, "message": "入站创建成功", "tag": tag, - "type": inboundType, + "type": requestMap["type"], }) } @@ -238,6 +292,9 @@ func (s *Server) removeInbound(w http.ResponseWriter, r *http.Request) { return } + // 删除配置 + delete(s.inboundConfigs, tag) + // 检查入站是否存在 _, exists := s.inbound.Get(tag) if !exists { @@ -259,6 +316,13 @@ func (s *Server) removeInbound(w http.ResponseWriter, r *http.Request) { return } + // 保存配置到文件 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.JSON(w, r, map[string]interface{}{ "success": true, "message": "入站移除成功", @@ -303,16 +367,16 @@ func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) { return } - // 提取tag和type - tag, tagExists := requestMap["tag"].(string) - outboundType, typeExists := requestMap["type"].(string) - - if !tagExists || !typeExists { + tag, ok := requestMap["tag"].(string) + if !ok { render.Status(r, http.StatusBadRequest) - render.JSON(w, r, map[string]string{"error": "tag和type不能为空"}) + render.JSON(w, r, map[string]string{"error": "tag不能为空"}) return } + // 保存原始配置 + s.outboundConfigs[tag] = requestMap + // 检查出站是否已存在 if _, exists := s.outbound.Outbound(tag); exists { render.Status(r, http.StatusConflict) @@ -336,7 +400,7 @@ func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) { } // 记录日志 - s.logger.Info("创建出站: ", outboundType, "[", tag, "]") + s.logger.Info("创建出站: ", requestMap["type"], "[", tag, "]") // 获取出站注册表 outboundRegistry := service.FromContext[adapter.OutboundRegistry](s.ctx) @@ -347,10 +411,10 @@ func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) { } // 创建出站配置对象 - optionsObj, exists := outboundRegistry.CreateOptions(outboundType) + optionsObj, exists := outboundRegistry.CreateOptions(requestMap["type"].(string)) if !exists { render.Status(r, http.StatusBadRequest) - render.JSON(w, r, map[string]string{"error": "不支持的出站类型: " + outboundType}) + render.JSON(w, r, map[string]string{"error": "不支持的出站类型: " + requestMap["type"].(string)}) return } @@ -370,19 +434,26 @@ func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) { } // 创建出站 - err = s.outbound.Create(s.ctx, s.router, s.logger, tag, outboundType, optionsObj) + err = s.outbound.Create(s.ctx, s.router, s.logger, tag, requestMap["type"].(string), optionsObj) if err != nil { render.Status(r, http.StatusBadRequest) render.JSON(w, r, map[string]string{"error": "创建出站失败: " + err.Error()}) return } + // 在成功创建出站后保存配置 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.Status(r, http.StatusOK) render.JSON(w, r, map[string]interface{}{ "success": true, "message": "出站创建成功", "tag": tag, - "type": outboundType, + "type": requestMap["type"], }) } @@ -395,6 +466,9 @@ func (s *Server) removeOutbound(w http.ResponseWriter, r *http.Request) { return } + // 删除配置 + delete(s.outboundConfigs, tag) + // 检查出站是否存在 _, exists := s.outbound.Outbound(tag) if !exists { @@ -416,6 +490,13 @@ func (s *Server) removeOutbound(w http.ResponseWriter, r *http.Request) { return } + // 保存配置到文件 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.JSON(w, r, map[string]interface{}{ "success": true, "message": "出站移除成功", @@ -480,6 +561,9 @@ func (s *Server) createRouteRule(w http.ResponseWriter, r *http.Request) { return } + // 保存原始配置 + s.ruleConfigs = append(s.ruleConfigs, requestMap) + // 检查outbound字段,这是必需的 outboundRaw, hasOutbound := requestMap["outbound"] if !hasOutbound { @@ -612,6 +696,13 @@ func (s *Server) createRouteRule(w http.ResponseWriter, r *http.Request) { // 使用Router的AddRule方法添加规则 ruleIndex := s.router.AddRule(adapterRule) + // 在成功创建规则后保存配置 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.Status(r, http.StatusOK) render.JSON(w, r, map[string]interface{}{ "success": true, @@ -638,6 +729,11 @@ func (s *Server) removeRouteRule(w http.ResponseWriter, r *http.Request) { return } + // 删除配置 + if index >= 0 && index < len(s.ruleConfigs) { + s.ruleConfigs = append(s.ruleConfigs[:index], s.ruleConfigs[index+1:]...) + } + // 使用Router的RemoveRule方法移除规则 err = s.router.RemoveRule(index) if err != nil { @@ -646,6 +742,13 @@ func (s *Server) removeRouteRule(w http.ResponseWriter, r *http.Request) { return } + // 保存配置到文件 + if s.enableConfigSave { + if err := s.saveConfig(); err != nil { + s.logger.Warn("保存配置失败:", err) + } + } + render.Status(r, http.StatusOK) render.JSON(w, r, map[string]interface{}{ "success": true, @@ -676,3 +779,372 @@ func (s *Server) listRouteRules(w http.ResponseWriter, r *http.Request) { "rules": rules, }) } + +// saveConfig 保存当前的动态配置到文件 +func (s *Server) saveConfig() error { + if !s.enableConfigSave || s.configSavePath == "" { + return nil + } + + // 构建配置 + config := &DynamicConfig{ + Inbounds: make([]map[string]interface{}, 0), + Outbounds: make([]map[string]interface{}, 0), + Rules: make([]map[string]interface{}, 0), + } + + // 获取所有入站配置 + for _, inbound := range s.inbound.Inbounds() { + // 跳过初始配置中的入站 + if s.initialInbounds[inbound.Tag()] { + s.logger.Debug("跳过初始入站:", inbound.Tag()) + continue + } + + // 跳过系统入站(如tun) + if inbound.Type() == "tun" { + s.logger.Debug("跳过系统入站:", inbound.Tag(), "(", inbound.Type(), ")") + continue + } + + // 获取保存的配置 + if savedConfig, exists := s.inboundConfigs[inbound.Tag()]; exists { + config.Inbounds = append(config.Inbounds, savedConfig) + } + } + + // 获取所有出站配置 + for _, outbound := range s.outbound.Outbounds() { + // 跳过初始配置中的出站 + if s.initialOutbounds[outbound.Tag()] { + s.logger.Debug("跳过初始出站:", outbound.Tag()) + continue + } + + // 获取保存的配置 + if savedConfig, exists := s.outboundConfigs[outbound.Tag()]; exists { + config.Outbounds = append(config.Outbounds, savedConfig) + } + } + + // 获取所有规则配置 + config.Rules = s.ruleConfigs + + // 将配置写入文件,使用缩进格式化 + configData, err := json.MarshalIndent(config, "", " ") + if err != nil { + s.logger.Error("序列化最终配置失败:", err) + return err + } + + s.logger.Debug("保存的完整配置:", string(configData)) + return os.WriteFile(s.configSavePath, configData, 0644) +} + +// 添加配置重载相关的辅助方法 +func (s *Server) createInboundFromConfig(config map[string]interface{}) error { + tag, ok := config["tag"].(string) + if !ok { + return errors.New("入站配置缺少tag字段") + } + + inboundType, ok := config["type"].(string) + if !ok { + return errors.New("入站配置缺少type字段") + } + + // 获取入站注册表 + inboundRegistry := service.FromContext[adapter.InboundRegistry](s.ctx) + if inboundRegistry == nil { + return errors.New("入站注册服务不可用") + } + + // 创建入站配置对象 + optionsObj, exists := inboundRegistry.CreateOptions(inboundType) + if !exists { + return errors.New("不支持的入站类型: " + inboundType) + } + + // 将配置转换为正确的结构体 + optionsJson, err := json.Marshal(config) + if err != nil { + return err + } + + err = json.Unmarshal(optionsJson, optionsObj) + if err != nil { + return err + } + + // 创建入站 + return s.inbound.Create(s.ctx, s.router, s.logger, tag, inboundType, optionsObj) +} + +func (s *Server) createOutboundFromConfig(config map[string]interface{}) error { + tag, ok := config["tag"].(string) + if !ok { + return errors.New("出站配置缺少tag字段") + } + + outboundType, ok := config["type"].(string) + if !ok { + return errors.New("出站配置缺少type字段") + } + + // 获取出站注册表 + outboundRegistry := service.FromContext[adapter.OutboundRegistry](s.ctx) + if outboundRegistry == nil { + return errors.New("出站注册服务不可用") + } + + // 创建出站配置对象 + optionsObj, exists := outboundRegistry.CreateOptions(outboundType) + if !exists { + return errors.New("不支持的出站类型: " + outboundType) + } + + // 将配置转换为正确的结构体 + optionsJson, err := json.Marshal(config) + if err != nil { + return err + } + + err = json.Unmarshal(optionsJson, optionsObj) + if err != nil { + return err + } + + // 创建出站 + return s.outbound.Create(s.ctx, s.router, s.logger, tag, outboundType, optionsObj) +} + +func (s *Server) createRuleFromConfig(config map[string]interface{}) error { + ruleJson, err := json.Marshal(config) + if err != nil { + return err + } + + var rule option.Rule + if err := json.Unmarshal(ruleJson, &rule); err != nil { + return err + } + + adapterRule, err := R.NewRule(s.ctx, s.logger, rule, true) + if err != nil { + return err + } + + s.router.AddRule(adapterRule) + return nil +} + +// loadConfig 从文件加载配置 +func (s *Server) loadConfig() error { + if !s.enableConfigSave || s.configSavePath == "" { + return nil + } + + // 读取配置文件 + configData, err := os.ReadFile(s.configSavePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + var config DynamicConfig + if err := json.Unmarshal(configData, &config); err != nil { + return err + } + + // 获取当前所有入站 + currentInbounds := s.inbound.Inbounds() + systemInbounds := make(map[string]bool) + + // 标记系统入站(如tun) + for _, inbound := range currentInbounds { + if inbound.Type() == "tun" { + systemInbounds[inbound.Tag()] = true + continue + } + } + + // 清除现有配置,但保留系统入站 + for _, inbound := range currentInbounds { + if !systemInbounds[inbound.Tag()] { + s.inbound.Remove(inbound.Tag()) + } + } + + // 清除所有出站 + for _, outbound := range s.outbound.Outbounds() { + s.outbound.Remove(outbound.Tag()) + } + + // 清除所有规则 + rules := s.router.Rules() + for i := len(rules) - 1; i >= 0; i-- { + s.router.RemoveRule(i) + } + + // 重新加载入站配置,跳过与系统入站冲突的配置 + for _, inboundConfig := range config.Inbounds { + tag, ok := inboundConfig["tag"].(string) + if !ok { + s.logger.Warn("入站配置缺少tag字段") + continue + } + + // 如果是系统入站,跳过 + if systemInbounds[tag] { + s.logger.Info("跳过系统入站配置:", tag) + continue + } + + if err := s.createInboundFromConfig(inboundConfig); err != nil { + s.logger.Warn("加载入站配置失败:", err) + } + } + + // 重新加载出站配置 + for _, outboundConfig := range config.Outbounds { + if err := s.createOutboundFromConfig(outboundConfig); err != nil { + s.logger.Warn("加载出站配置失败:", err) + } + } + + // 重新加载规则配置 + for _, ruleConfig := range config.Rules { + if err := s.createRuleFromConfig(ruleConfig); err != nil { + s.logger.Warn("加载规则配置失败:", err) + } + } + + return nil +} + +// handleTestOutbound 处理出站测试请求 +func (s *Server) handleTestOutbound(w http.ResponseWriter, r *http.Request) { + var request TestOutboundRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "无法解析请求: " + err.Error()}) + return + } + + // 获取出站 + outbound, exists := s.outbound.Outbound(request.Tag) + if !exists || outbound == nil { + render.Status(r, http.StatusNotFound) + render.JSON(w, r, map[string]string{"error": "出站不存在: " + request.Tag}) + return + } + + // 如果没有提供测试URL,使用默认的 + if request.TestURL == "" { + request.TestURL = "http://www.gstatic.com/generate_204" + } + + // 验证URL格式 + if _, err := url.Parse(request.TestURL); err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, map[string]string{"error": "无效的URL: " + err.Error()}) + return + } + + // 创建HTTP客户端 + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + // 解析地址 + host, port, err := net.SplitHostPort(addr) + if err != nil { + host = addr + switch network { + case "tcp", "tcp4", "tcp6": + port = "80" + default: + return nil, E.New("unsupported network: ", network) + } + } + + // 创建 Socksaddr + portNum, err := strconv.Atoi(port) + if err != nil { + return nil, err + } + + destination := metadata.Socksaddr{ + Fqdn: host, + Port: uint16(portNum), + } + + // 如果是IP地址,则解析它 + if ip := net.ParseIP(host); ip != nil { + if addr, err := netip.ParseAddr(host); err == nil { + destination.Addr = addr + destination.Fqdn = "" + } else { + return nil, E.New("invalid IP address: ", host) + } + } + + return outbound.DialContext(ctx, network, destination) + }, + // 禁用默认的TLS验证,因为某些代理可能使用自签名证书 + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + // 记录开始时间 + startTime := time.Now() + + // 发送测试请求 + resp, err := client.Get(request.TestURL) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, map[string]string{ + "status": "failed", + "error": err.Error(), + }) + return + } + defer resp.Body.Close() + + // 计算延迟 + delay := time.Since(startTime) + + // 读取响应体(但限制大小) + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) + + // 返回测试结果 + render.JSON(w, r, map[string]interface{}{ + "status": "success", + "delay_ms": delay.Milliseconds(), + "status_code": resp.StatusCode, + "body_length": len(body), + "headers": resp.Header, + }) +} + +// 添加新的API路由处理函数 +func (s *Server) handleSaveConfig(w http.ResponseWriter, r *http.Request) { + if err := s.saveConfig(); err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, map[string]string{"error": "保存配置失败: " + err.Error()}) + return + } + render.JSON(w, r, map[string]string{"message": "配置已保存"}) +} + +func (s *Server) handleReloadConfig(w http.ResponseWriter, r *http.Request) { + if err := s.loadConfig(); err != nil { + render.Status(r, http.StatusInternalServerError) + render.JSON(w, r, map[string]string{"error": "重载配置失败: " + err.Error()}) + return + } + render.JSON(w, r, map[string]string{"message": "配置已重载"}) +} diff --git a/option/dynamic.go b/option/dynamic.go index 646ef4e8..d2b88969 100644 --- a/option/dynamic.go +++ b/option/dynamic.go @@ -5,4 +5,8 @@ type DynamicAPIOptions struct { Listen string `json:"listen"` // API认证密钥 Secret string `json:"secret"` + // 是否启用配置保存 + EnableConfigSave bool `json:"enable_config_save,omitempty"` + // 配置文件保存路径 + ConfigSavePath string `json:"config_save_path,omitempty"` }