mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
添加保存配置和重载配置,测试出站连接api
This commit is contained in:
parent
21a4bf8edb
commit
47cba95566
219
README.md
219
README.md
@ -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
240
docs/dynamic-api.md
Normal 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`)
|
@ -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"
|
||||||
@ -36,6 +40,27 @@ type Server struct {
|
|||||||
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": "配置已重载"})
|
||||||
|
}
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user