添加保存配置和重载配置,测试出站连接api

This commit is contained in:
mingtianquan 2025-04-20 23:11:29 +08:00
parent 21a4bf8edb
commit 47cba95566
4 changed files with 747 additions and 250 deletions

219
README.md
View File

@ -8,225 +8,6 @@ The universal proxy platform.
https://sing-box.sagernet.org 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 ## License

240
docs/dynamic-api.md Normal file
View File

@ -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`

View File

@ -2,11 +2,14 @@ package dynamicapi
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -17,6 +20,7 @@ import (
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
R "github.com/sagernet/sing-box/route/rule" R "github.com/sagernet/sing-box/route/rule"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -27,15 +31,36 @@ import (
var _ adapter.DynamicManager = (*Server)(nil) var _ adapter.DynamicManager = (*Server)(nil)
type Server struct { type Server struct {
ctx context.Context ctx context.Context
router adapter.Router router adapter.Router
inbound adapter.InboundManager inbound adapter.InboundManager
outbound adapter.OutboundManager outbound adapter.OutboundManager
logger log.ContextLogger logger log.ContextLogger
logFactory log.Factory logFactory log.Factory
httpServer *http.Server httpServer *http.Server
listenAddress string listenAddress string
secret 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) { 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, Addr: options.Listen,
Handler: r, 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) r.Use(middleware.Logger)
@ -79,6 +119,7 @@ func NewServer(ctx context.Context, logger log.ContextLogger, options option.Dyn
r.Post("/", s.createOutbound) r.Post("/", s.createOutbound)
r.Delete("/{tag}", s.removeOutbound) r.Delete("/{tag}", s.removeOutbound)
r.Get("/", s.listOutbounds) r.Get("/", s.listOutbounds)
r.Post("/test", s.handleTestOutbound)
}) })
// 路由规则API // 路由规则API
@ -87,6 +128,12 @@ func NewServer(ctx context.Context, logger log.ContextLogger, options option.Dyn
r.Delete("/rule/{index}", s.removeRouteRule) r.Delete("/rule/{index}", s.removeRouteRule)
r.Get("/rules", s.listRouteRules) 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 return s, nil
@ -146,16 +193,16 @@ func (s *Server) createInbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 提取tag和type tag, ok := requestMap["tag"].(string)
tag, tagExists := requestMap["tag"].(string) if !ok {
inboundType, typeExists := requestMap["type"].(string)
if !tagExists || !typeExists {
render.Status(r, http.StatusBadRequest) 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 return
} }
// 保存原始配置
s.inboundConfigs[tag] = requestMap
// 检查入站是否已存在 // 检查入站是否已存在
if _, exists := s.inbound.Get(tag); exists { if _, exists := s.inbound.Get(tag); exists {
render.Status(r, http.StatusConflict) 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) 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 { if !exists {
render.Status(r, http.StatusBadRequest) 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 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 { if err != nil {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)
render.JSON(w, r, map[string]string{"error": "创建入站失败: " + err.Error()}) render.JSON(w, r, map[string]string{"error": "创建入站失败: " + err.Error()})
return return
} }
// 在成功创建入站后保存配置
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
"message": "入站创建成功", "message": "入站创建成功",
"tag": tag, "tag": tag,
"type": inboundType, "type": requestMap["type"],
}) })
} }
@ -238,6 +292,9 @@ func (s *Server) removeInbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 删除配置
delete(s.inboundConfigs, tag)
// 检查入站是否存在 // 检查入站是否存在
_, exists := s.inbound.Get(tag) _, exists := s.inbound.Get(tag)
if !exists { if !exists {
@ -259,6 +316,13 @@ func (s *Server) removeInbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 保存配置到文件
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
"message": "入站移除成功", "message": "入站移除成功",
@ -303,16 +367,16 @@ func (s *Server) createOutbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 提取tag和type tag, ok := requestMap["tag"].(string)
tag, tagExists := requestMap["tag"].(string) if !ok {
outboundType, typeExists := requestMap["type"].(string)
if !tagExists || !typeExists {
render.Status(r, http.StatusBadRequest) 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 return
} }
// 保存原始配置
s.outboundConfigs[tag] = requestMap
// 检查出站是否已存在 // 检查出站是否已存在
if _, exists := s.outbound.Outbound(tag); exists { if _, exists := s.outbound.Outbound(tag); exists {
render.Status(r, http.StatusConflict) 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) 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 { if !exists {
render.Status(r, http.StatusBadRequest) 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 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 { if err != nil {
render.Status(r, http.StatusBadRequest) render.Status(r, http.StatusBadRequest)
render.JSON(w, r, map[string]string{"error": "创建出站失败: " + err.Error()}) render.JSON(w, r, map[string]string{"error": "创建出站失败: " + err.Error()})
return return
} }
// 在成功创建出站后保存配置
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
"message": "出站创建成功", "message": "出站创建成功",
"tag": tag, "tag": tag,
"type": outboundType, "type": requestMap["type"],
}) })
} }
@ -395,6 +466,9 @@ func (s *Server) removeOutbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 删除配置
delete(s.outboundConfigs, tag)
// 检查出站是否存在 // 检查出站是否存在
_, exists := s.outbound.Outbound(tag) _, exists := s.outbound.Outbound(tag)
if !exists { if !exists {
@ -416,6 +490,13 @@ func (s *Server) removeOutbound(w http.ResponseWriter, r *http.Request) {
return return
} }
// 保存配置到文件
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
"message": "出站移除成功", "message": "出站移除成功",
@ -480,6 +561,9 @@ func (s *Server) createRouteRule(w http.ResponseWriter, r *http.Request) {
return return
} }
// 保存原始配置
s.ruleConfigs = append(s.ruleConfigs, requestMap)
// 检查outbound字段这是必需的 // 检查outbound字段这是必需的
outboundRaw, hasOutbound := requestMap["outbound"] outboundRaw, hasOutbound := requestMap["outbound"]
if !hasOutbound { if !hasOutbound {
@ -612,6 +696,13 @@ func (s *Server) createRouteRule(w http.ResponseWriter, r *http.Request) {
// 使用Router的AddRule方法添加规则 // 使用Router的AddRule方法添加规则
ruleIndex := s.router.AddRule(adapterRule) ruleIndex := s.router.AddRule(adapterRule)
// 在成功创建规则后保存配置
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
@ -638,6 +729,11 @@ func (s *Server) removeRouteRule(w http.ResponseWriter, r *http.Request) {
return return
} }
// 删除配置
if index >= 0 && index < len(s.ruleConfigs) {
s.ruleConfigs = append(s.ruleConfigs[:index], s.ruleConfigs[index+1:]...)
}
// 使用Router的RemoveRule方法移除规则 // 使用Router的RemoveRule方法移除规则
err = s.router.RemoveRule(index) err = s.router.RemoveRule(index)
if err != nil { if err != nil {
@ -646,6 +742,13 @@ func (s *Server) removeRouteRule(w http.ResponseWriter, r *http.Request) {
return return
} }
// 保存配置到文件
if s.enableConfigSave {
if err := s.saveConfig(); err != nil {
s.logger.Warn("保存配置失败:", err)
}
}
render.Status(r, http.StatusOK) render.Status(r, http.StatusOK)
render.JSON(w, r, map[string]interface{}{ render.JSON(w, r, map[string]interface{}{
"success": true, "success": true,
@ -676,3 +779,372 @@ func (s *Server) listRouteRules(w http.ResponseWriter, r *http.Request) {
"rules": rules, "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": "配置已重载"})
}

View File

@ -5,4 +5,8 @@ type DynamicAPIOptions struct {
Listen string `json:"listen"` Listen string `json:"listen"`
// API认证密钥 // API认证密钥
Secret string `json:"secret"` Secret string `json:"secret"`
// 是否启用配置保存
EnableConfigSave bool `json:"enable_config_save,omitempty"`
// 配置文件保存路径
ConfigSavePath string `json:"config_save_path,omitempty"`
} }