mirror of
https://github.com/nitezs/sub2sing-box.git
synced 2024-12-23 20:24:42 -05:00
add: 国家策略组功能
This commit is contained in:
parent
3c180ae61e
commit
891db1975f
66
README.md
66
README.md
@ -10,41 +10,53 @@
|
|||||||
|
|
||||||
## api
|
## api
|
||||||
|
|
||||||
##### GET /convert
|
### GET /convert?data=xxx
|
||||||
|
|
||||||
- `data`: Base64 编码(url safe)的 JSON 字符串,包含以下字段:
|
data 为 base64 URL 编码的请求体,示例
|
||||||
- `subscription`: []string
|
|
||||||
- `proxy`: []string
|
|
||||||
- `delete`: string 可选
|
|
||||||
- `rename`: string 可选
|
|
||||||
- `template`: map[string]string 可选
|
|
||||||
|
|
||||||
示例
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"subscription": ["url1", "url2"],
|
"subscriptions": ["订阅地址1", "订阅地址2"],
|
||||||
"proxy": ["p1", "p2"],
|
"proxies": ["代理1", "代理2"],
|
||||||
"delete": "reg",
|
"template": "模板路径",
|
||||||
"template": "t",
|
"delete": "",
|
||||||
"rename": {
|
"rename": {"原文本": "新文本"},
|
||||||
"text": "replaceTo"
|
"group": false,
|
||||||
}
|
"group-type": "selector",
|
||||||
|
"sort": "name",
|
||||||
|
"sort-type": "asc"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Template
|
## Template 占位符
|
||||||
|
|
||||||
Template 中使用 `<all-proxy-tags>` 指明节点插入位置,例如
|
- `<all-proxy-tags>`: 插入所有节点标签
|
||||||
|
```
|
||||||
```
|
{
|
||||||
{
|
"type": "selector",
|
||||||
"type": "selector",
|
"tag": "节点选择",
|
||||||
"tag": "节点选择",
|
"outbounds": ["<all-proxy-tags>", "direct"],
|
||||||
"outbounds": ["<all-proxy-tags>", "direct"],
|
"interrupt_exist_connections": true
|
||||||
"interrupt_exist_connections": true
|
}
|
||||||
},
|
```
|
||||||
```
|
- `<all-country-tags>`: 插入所有国家标签
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "节点选择",
|
||||||
|
"outbounds": ["<all-country-tags>", "direct"],
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `<国家(地区)二字码>`: 插入国家(地区)所有节点标签,例如 `<tw>`
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "巴哈姆特",
|
||||||
|
"outbounds": ["<tw>", "direct"],
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
@ -38,7 +38,17 @@ func Convert(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := putil.Convert(data.Subscriptions, data.Proxies, data.Template, data.Delete, data.Rename)
|
result, err := putil.Convert(
|
||||||
|
data.Subscriptions,
|
||||||
|
data.Proxies,
|
||||||
|
data.Template,
|
||||||
|
data.Delete,
|
||||||
|
data.Rename,
|
||||||
|
data.Group,
|
||||||
|
data.GroupType,
|
||||||
|
data.SortKey,
|
||||||
|
data.SortType,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
|
@ -6,4 +6,8 @@ type ConvertRequest struct {
|
|||||||
Template string `form:"template" json:"template"`
|
Template string `form:"template" json:"template"`
|
||||||
Delete string `form:"delete" json:"delete"`
|
Delete string `form:"delete" json:"delete"`
|
||||||
Rename map[string]string `form:"rename" json:"rename"`
|
Rename map[string]string `form:"rename" json:"rename"`
|
||||||
|
Group bool `form:"group" json:"group"`
|
||||||
|
GroupType string `form:"group-type" json:"group-type"`
|
||||||
|
SortKey string `form:"sort" json:"sort"`
|
||||||
|
SortType string `form:"sort-type" json:"sort-type"`
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" data-bs-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@ -17,73 +17,144 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container mt-5">
|
<div class="container my-5">
|
||||||
<h2>sub2sing-box</h2>
|
<h2>
|
||||||
<div id="form">
|
<a
|
||||||
<!-- Subscription -->
|
href="https://github.com/nitezs/sub2sing-box"
|
||||||
<div class="form-group">
|
target="_blank"
|
||||||
<label for="subscription">Subscription:</label>
|
class="text-decoration-none"
|
||||||
<textarea
|
>sub2sing-box</a
|
||||||
class="form-control"
|
|
||||||
id="subscription"
|
|
||||||
name="subscription"
|
|
||||||
placeholder="一行一个"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Proxy -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="proxy">Proxy:</label>
|
|
||||||
<textarea
|
|
||||||
class="form-control"
|
|
||||||
id="proxy"
|
|
||||||
name="proxy"
|
|
||||||
placeholder="一行一个"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="delete">Delete:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="delete"
|
|
||||||
name="delete"
|
|
||||||
placeholder="支持正则表达式"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Template -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="template">Template:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
id="template"
|
|
||||||
name="template"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rename -->
|
|
||||||
<label for="renameContainer">Rename:</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary mb-2"
|
|
||||||
onclick="addRenameField()"
|
|
||||||
>
|
>
|
||||||
+
|
</h2>
|
||||||
</button>
|
<div id="form">
|
||||||
<div id="renameContainer"></div>
|
<div class="card my-4">
|
||||||
</div>
|
<div class="card-header">节点</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Subscription -->
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">订阅链接</span>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="subscription"
|
||||||
|
name="subscription"
|
||||||
|
placeholder="一行一个"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Output -->
|
<!-- Proxy -->
|
||||||
<div class="form-group">
|
<div class="input-group mb-3">
|
||||||
<label for="output">Link:</label>
|
<span class="input-group-text">节点分享链接</span>
|
||||||
<textarea class="form-control" id="output" name="output"></textarea>
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="proxy"
|
||||||
|
name="proxy"
|
||||||
|
placeholder="一行一个"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete -->
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">删除节点:</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="delete"
|
||||||
|
name="delete"
|
||||||
|
placeholder="支持正则表达式"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rename -->
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<span class="input-group-text">重命名节点</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
onclick="addRenameField()"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="renameContainer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">模板</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Template -->
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="template"
|
||||||
|
name="template"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card my-4">
|
||||||
|
<div class="card-header">国家策略组</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Group -->
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="group"
|
||||||
|
name="group"
|
||||||
|
/>
|
||||||
|
<label for="group">启用</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GroupType -->
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">类型</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="group-type"
|
||||||
|
name="group-type"
|
||||||
|
value="selector"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sort -->
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">排序依据</span>
|
||||||
|
<select class="form-select" name="sort" id="sort">
|
||||||
|
<option value="tag" selected>节点名</option>
|
||||||
|
<option value="num">节点数量</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SortType -->
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">排序方式</span>
|
||||||
|
<select class="form-select" name="sort-type" id="sort-type">
|
||||||
|
<option value="asc" selected>升序</option>
|
||||||
|
<option value="desc">降序</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">生成链接</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Output -->
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="output"
|
||||||
|
name="output"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<link
|
<link
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@ -130,6 +201,10 @@
|
|||||||
for (let input of inputs) {
|
for (let input of inputs) {
|
||||||
input.addEventListener("input", generateLink);
|
input.addEventListener("input", generateLink);
|
||||||
}
|
}
|
||||||
|
const selects = document.querySelectorAll("#form select");
|
||||||
|
for (let select of selects) {
|
||||||
|
select.addEventListener("change", generateLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanLisnter() {
|
function cleanLisnter() {
|
||||||
@ -137,14 +212,18 @@
|
|||||||
for (let input of inputs) {
|
for (let input of inputs) {
|
||||||
input.removeEventListener("input", generateLink);
|
input.removeEventListener("input", generateLink);
|
||||||
}
|
}
|
||||||
|
const selects = document.querySelectorAll("#form select");
|
||||||
|
for (let select of selects) {
|
||||||
|
select.removeEventListener("change", generateLink);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRenameField() {
|
function addRenameField() {
|
||||||
cleanLisnter();
|
cleanLisnter();
|
||||||
const container = document.getElementById("renameContainer");
|
const container = document.getElementById("renameContainer");
|
||||||
const fieldHTML = `<div class="rename-group d-flex align-items-center">
|
const fieldHTML = `<div class="rename-group input-group">
|
||||||
<input type="text" class="form-control mr-2" name="rename_from[]" placeholder="Old Name">
|
<input type="text" class="form-control" name="rename_from[]" placeholder="原字符(支持正则表达式)">
|
||||||
<input type="text" class="form-control mr-2" name="rename_to[]" placeholder="New Name">
|
<input type="text" class="form-control" name="rename_to[]" placeholder="替换字符">
|
||||||
<button type="button" class="btn btn-danger" onclick="removeThisField(this)">-</button>
|
<button type="button" class="btn btn-danger" onclick="removeThisField(this)">-</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
container.insertAdjacentHTML("beforeend", fieldHTML);
|
container.insertAdjacentHTML("beforeend", fieldHTML);
|
||||||
@ -176,6 +255,10 @@
|
|||||||
document.getElementsByName("rename_to[]")
|
document.getElementsByName("rename_to[]")
|
||||||
).map((input) => input.value);
|
).map((input) => input.value);
|
||||||
const output = document.getElementById("output");
|
const output = document.getElementById("output");
|
||||||
|
const group = document.getElementById("group").checked;
|
||||||
|
const groupType = document.getElementById("group-type").value;
|
||||||
|
const sort = document.getElementById("sort").value;
|
||||||
|
const sortType = document.getElementById("sort-type").value;
|
||||||
|
|
||||||
let rename = {};
|
let rename = {};
|
||||||
for (let i = 0; i < renameFrom.length; i++) {
|
for (let i = 0; i < renameFrom.length; i++) {
|
||||||
@ -189,6 +272,10 @@
|
|||||||
delete: deleteRule,
|
delete: deleteRule,
|
||||||
template,
|
template,
|
||||||
rename,
|
rename,
|
||||||
|
group,
|
||||||
|
"group-type": groupType,
|
||||||
|
sort,
|
||||||
|
"sort-type": sortType,
|
||||||
};
|
};
|
||||||
|
|
||||||
output.value = `${window.location.origin}/convert?data=${encodeBase64(
|
output.value = `${window.location.origin}/convert?data=${encodeBase64(
|
||||||
|
@ -14,6 +14,10 @@ var template string
|
|||||||
var output string
|
var output string
|
||||||
var delete string
|
var delete string
|
||||||
var rename map[string]string
|
var rename map[string]string
|
||||||
|
var group bool
|
||||||
|
var groupType string
|
||||||
|
var sortKey string
|
||||||
|
var sortType string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
convertCmd.Flags().StringSliceVarP(&subscriptions, "subscription", "s", []string{}, "subscription urls")
|
convertCmd.Flags().StringSliceVarP(&subscriptions, "subscription", "s", []string{}, "subscription urls")
|
||||||
@ -22,6 +26,10 @@ func init() {
|
|||||||
convertCmd.Flags().StringVarP(&output, "output", "o", "", "output file path")
|
convertCmd.Flags().StringVarP(&output, "output", "o", "", "output file path")
|
||||||
convertCmd.Flags().StringVarP(&delete, "delete", "d", "", "delete proxy with regex")
|
convertCmd.Flags().StringVarP(&delete, "delete", "d", "", "delete proxy with regex")
|
||||||
convertCmd.Flags().StringToStringVarP(&rename, "rename", "r", map[string]string{}, "rename proxy with regex")
|
convertCmd.Flags().StringToStringVarP(&rename, "rename", "r", map[string]string{}, "rename proxy with regex")
|
||||||
|
convertCmd.Flags().BoolVarP(&group, "group", "g", false, "group proxies by country")
|
||||||
|
convertCmd.Flags().StringVarP(&groupType, "group-type", "G", "selector", "group type, selector or urltest")
|
||||||
|
convertCmd.Flags().StringVarP(&sortKey, "sort", "S", "tag", "sort key, tag or num")
|
||||||
|
convertCmd.Flags().StringVarP(&sortType, "sort-type", "T", "asc", "sort type, asc or desc")
|
||||||
RootCmd.AddCommand(convertCmd)
|
RootCmd.AddCommand(convertCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +40,17 @@ var convertCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
result := ""
|
result := ""
|
||||||
var err error
|
var err error
|
||||||
result, err = Convert(subscriptions, proxies, template, delete, rename)
|
result, err = Convert(
|
||||||
|
subscriptions,
|
||||||
|
proxies,
|
||||||
|
template,
|
||||||
|
delete,
|
||||||
|
rename,
|
||||||
|
group,
|
||||||
|
groupType,
|
||||||
|
sortKey,
|
||||||
|
sortType,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6
|
// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6
|
||||||
// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81
|
// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81
|
||||||
|
|
||||||
@ -1042,3 +1048,41 @@ var CountryISO = map[string]string{
|
|||||||
"ZM": "赞比亚(ZM)",
|
"ZM": "赞比亚(ZM)",
|
||||||
"ZW": "津巴布韦(ZW)",
|
"ZW": "津巴布韦(ZW)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetContryName(tag string) string {
|
||||||
|
reg := regexp.MustCompile(`(\s?[A-Za-z]{2}[\s-_/]|[\(\[][A-Za-z]{2}[\)\]])`)
|
||||||
|
tagSlice := reg.FindStringSubmatch(tag)
|
||||||
|
for i := range tagSlice {
|
||||||
|
tagSlice[i] = strings.ToLower(strings.Trim(tagSlice[i], "()[] -_/"))
|
||||||
|
}
|
||||||
|
countryMaps := []map[string]string{
|
||||||
|
CountryFlag,
|
||||||
|
CountryChineseName,
|
||||||
|
CountryISO,
|
||||||
|
CountryEnglishName,
|
||||||
|
}
|
||||||
|
for _, countryMap := range countryMaps {
|
||||||
|
for k, v := range countryMap {
|
||||||
|
if slices.Contains(tagSlice, strings.ToLower(k)) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if strings.Contains(tag, strings.ToLower(k)) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "其他地区"
|
||||||
|
}
|
||||||
|
|
||||||
|
var values []string
|
||||||
|
|
||||||
|
func IsCountryGroup(tag string) bool {
|
||||||
|
return slices.Contains(values, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
values = make([]string, 0, len(CountryISO))
|
||||||
|
for _, v := range CountryISO {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
27
internal/model/sort.go
Normal file
27
internal/model/sort.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/collate"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortByNumber []Outbound
|
||||||
|
|
||||||
|
func (a SortByNumber) Len() int { return len(a) }
|
||||||
|
func (a SortByNumber) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a SortByNumber) Less(i, j int) bool { return len(a[i].Outbounds) < len(a[j].Outbounds) }
|
||||||
|
|
||||||
|
type SortByTag []Outbound
|
||||||
|
|
||||||
|
func (a SortByTag) Len() int { return len(a) }
|
||||||
|
func (a SortByTag) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a SortByTag) Less(i, j int) bool {
|
||||||
|
tags := []language.Tag{
|
||||||
|
language.English,
|
||||||
|
language.Chinese,
|
||||||
|
}
|
||||||
|
matcher := language.NewMatcher(tags)
|
||||||
|
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||||
|
c := collate.New(bestMatch)
|
||||||
|
return c.CompareString(a[i].Tag, a[j].Tag) < 0
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sub2sing-box/internal/model"
|
"sub2sing-box/internal/model"
|
||||||
@ -14,7 +15,17 @@ import (
|
|||||||
"sub2sing-box/pkg/parser"
|
"sub2sing-box/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Convert(subscriptions []string, proxies []string, template string, delete string, rename map[string]string) (string, error) {
|
func Convert(
|
||||||
|
subscriptions []string,
|
||||||
|
proxies []string,
|
||||||
|
template string,
|
||||||
|
delete string,
|
||||||
|
rename map[string]string,
|
||||||
|
group bool,
|
||||||
|
groupType string,
|
||||||
|
sortKey string,
|
||||||
|
sortType string,
|
||||||
|
) (string, error) {
|
||||||
result := ""
|
result := ""
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -78,14 +89,25 @@ func Convert(subscriptions []string, proxies []string, template string, delete s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxyList = newProxyList
|
proxyList = newProxyList
|
||||||
|
var outbounds []model.Outbound
|
||||||
|
ps, err := json.Marshal(&proxyList)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(ps, &outbounds)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if group {
|
||||||
|
outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType)
|
||||||
|
}
|
||||||
if template != "" {
|
if template != "" {
|
||||||
result, err = MergeTemplate(proxyList, template)
|
result, err = MergeTemplate(outbounds, template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
r, err := json.Marshal(proxyList)
|
r, err := json.Marshal(outbounds)
|
||||||
result = string(r)
|
result = string(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -95,7 +117,51 @@ func Convert(subscriptions []string, proxies []string, template string, delete s
|
|||||||
return string(result), nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MergeTemplate(proxies []model.Proxy, template string) (string, error) {
|
func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound {
|
||||||
|
newGroup := make(map[string]model.Outbound)
|
||||||
|
for _, p := range proxies {
|
||||||
|
if p.Type != "selector" && p.Type != "urltest" {
|
||||||
|
country := model.GetContryName(p.Tag)
|
||||||
|
if group, ok := newGroup[country]; ok {
|
||||||
|
group.Outbounds = append(group.Outbounds, p.Tag)
|
||||||
|
newGroup[country] = group
|
||||||
|
} else {
|
||||||
|
newGroup[country] = model.Outbound{
|
||||||
|
Tag: country,
|
||||||
|
Type: groupType,
|
||||||
|
Outbounds: []string{p.Tag},
|
||||||
|
InterruptExistConnections: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var groups []model.Outbound
|
||||||
|
for _, p := range newGroup {
|
||||||
|
groups = append(groups, p)
|
||||||
|
}
|
||||||
|
if sortType == "asc" {
|
||||||
|
switch sortKey {
|
||||||
|
case "tag":
|
||||||
|
sort.Sort(model.SortByTag(groups))
|
||||||
|
case "num":
|
||||||
|
sort.Sort(model.SortByNumber(groups))
|
||||||
|
default:
|
||||||
|
sort.Sort(model.SortByTag(groups))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch sortKey {
|
||||||
|
case "tag":
|
||||||
|
sort.Sort(sort.Reverse(model.SortByTag(groups)))
|
||||||
|
case "num":
|
||||||
|
sort.Sort(sort.Reverse(model.SortByNumber(groups)))
|
||||||
|
default:
|
||||||
|
sort.Sort(sort.Reverse(model.SortByTag(groups)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(proxies, groups...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeTemplate(outbounds []model.Outbound, template string) (string, error) {
|
||||||
var config model.Config
|
var config model.Config
|
||||||
var err error
|
var err error
|
||||||
if strings.HasPrefix(template, "http") {
|
if strings.HasPrefix(template, "http") {
|
||||||
@ -116,34 +182,41 @@ func MergeTemplate(proxies []model.Proxy, template string) (string, error) {
|
|||||||
config, err = ReadTemplate(template)
|
config, err = ReadTemplate(template)
|
||||||
}
|
}
|
||||||
proxyTags := make([]string, 0)
|
proxyTags := make([]string, 0)
|
||||||
|
groupTags := make([]string, 0)
|
||||||
|
groups := make(map[string]model.Outbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
for _, p := range proxies {
|
for _, p := range outbounds {
|
||||||
proxyTags = append(proxyTags, p.Tag)
|
if model.IsCountryGroup(p.Tag) {
|
||||||
}
|
groupTags = append(groupTags, p.Tag)
|
||||||
ps, err := json.Marshal(&proxies)
|
reg := regexp.MustCompile("[A-Za-z]{2}")
|
||||||
if err != nil {
|
country := reg.FindString(p.Tag)
|
||||||
return "", err
|
groups[country] = p
|
||||||
}
|
} else {
|
||||||
var newOutbounds []model.Outbound
|
proxyTags = append(proxyTags, p.Tag)
|
||||||
err = json.Unmarshal(ps, &newOutbounds)
|
}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
reg := regexp.MustCompile("<[A-Za-z]{2}>")
|
||||||
for i, outbound := range config.Outbounds {
|
for i, outbound := range config.Outbounds {
|
||||||
var parsedOutbound []string = make([]string, 0)
|
var parsedOutbound []string = make([]string, 0)
|
||||||
for _, o := range outbound.Outbounds {
|
for _, o := range outbound.Outbounds {
|
||||||
if o == "<all-proxy-tags>" {
|
if o == "<all-proxy-tags>" {
|
||||||
parsedOutbound = append(parsedOutbound, proxyTags...)
|
parsedOutbound = append(parsedOutbound, proxyTags...)
|
||||||
|
} else if o == "<all-country-tags>" {
|
||||||
|
parsedOutbound = append(parsedOutbound, groupTags...)
|
||||||
|
} else if reg.MatchString(o) {
|
||||||
|
country := strings.ToUpper(strings.Trim(reg.FindString(o), "<>"))
|
||||||
|
if group, ok := groups[country]; ok {
|
||||||
|
parsedOutbound = append(parsedOutbound, group.Outbounds...)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
parsedOutbound = append(parsedOutbound, o)
|
parsedOutbound = append(parsedOutbound, o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.Outbounds[i].Outbounds = parsedOutbound
|
config.Outbounds[i].Outbounds = parsedOutbound
|
||||||
}
|
}
|
||||||
config.Outbounds = append(config.Outbounds, newOutbounds...)
|
config.Outbounds = append(config.Outbounds, outbounds...)
|
||||||
//TODO: 国家策略组
|
|
||||||
data, err := json.Marshal(config)
|
data, err := json.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -254,20 +327,3 @@ func RenameProxy(proxies []model.Proxy, regex string, replaceText string) ([]mod
|
|||||||
}
|
}
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContryName(proxyName string) string {
|
|
||||||
countryMaps := []map[string]string{
|
|
||||||
model.CountryFlag,
|
|
||||||
model.CountryChineseName,
|
|
||||||
model.CountryISO,
|
|
||||||
model.CountryEnglishName,
|
|
||||||
}
|
|
||||||
for _, countryMap := range countryMaps {
|
|
||||||
for k, v := range countryMap {
|
|
||||||
if strings.Contains(proxyName, k) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "其他地区"
|
|
||||||
}
|
|
||||||
|
266
templates/tun-fakeip-without-dns-leaks-country-group.json
Normal file
266
templates/tun-fakeip-without-dns-leaks-country-group.json
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "info",
|
||||||
|
"timestamp": true
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"tag": "google",
|
||||||
|
"address": "tls://8.8.8.8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "local",
|
||||||
|
"address": "https://223.5.5.5/dns-query",
|
||||||
|
"detour": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "remote",
|
||||||
|
"address": "fakeip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"outbound": "any",
|
||||||
|
"server": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query_type": ["A", "AAAA"],
|
||||||
|
"server": "remote"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clash_mode": "Direct",
|
||||||
|
"server": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clash_mode": "Global",
|
||||||
|
"server": "google"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-geolocation-cn",
|
||||||
|
"server": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "and",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-geolocation-!cn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geoip-cn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"server": "google",
|
||||||
|
"client_subnet": "114.114.114.114"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fakeip": {
|
||||||
|
"enabled": true,
|
||||||
|
"inet4_range": "198.18.0.0/15",
|
||||||
|
"inet6_range": "fc00::/18"
|
||||||
|
},
|
||||||
|
"independent_cache": true
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"rule_set": [
|
||||||
|
{
|
||||||
|
"tag": "geosite-geolocation-cn",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-geolocation-!cn",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geoip-cn",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-category-ads-all",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-microsoft",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-microsoft.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-bilibili",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-bilibili.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-bahamut",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-bahamut.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-category-games@cn",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-games@cn.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "geosite-category-games",
|
||||||
|
"type": "remote",
|
||||||
|
"format": "binary",
|
||||||
|
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-games.srs",
|
||||||
|
"download_detour": "节点选择"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"type": "logical",
|
||||||
|
"mode": "or",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"protocol": "dns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"port": 53
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbound": "dns-out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip_is_private": true,
|
||||||
|
"outbound": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": ["geoip-cn", "geosite-geolocation-cn"],
|
||||||
|
"outbound": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-category-ads-all",
|
||||||
|
"outbound": "Ads"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-microsoft",
|
||||||
|
"outbound": "Microsoft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-bilibili",
|
||||||
|
"outbound": "Bilibili"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-category-games@cn",
|
||||||
|
"outbound": "Games(中国)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-category-games",
|
||||||
|
"outbound": "Games(全球)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule_set": "geosite-bahamut",
|
||||||
|
"outbound": "Bahamut"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "节点选择",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"inet4_address": "172.19.0.1/30",
|
||||||
|
"inet6_address": "fdfe:dcba:9876::1/126",
|
||||||
|
"auto_route": true,
|
||||||
|
"strict_route": true,
|
||||||
|
"sniff": true,
|
||||||
|
"sniff_override_destination": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "节点选择",
|
||||||
|
"outbounds": ["<all-country-tags>", "direct"],
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Ads",
|
||||||
|
"outbounds": ["direct", "block"],
|
||||||
|
"default": "block",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Microsoft",
|
||||||
|
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||||
|
"default": "节点选择",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Bilibili",
|
||||||
|
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||||
|
"default": "direct",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Games(全球)",
|
||||||
|
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||||
|
"default": "节点选择",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Games(中国)",
|
||||||
|
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||||
|
"default": "direct",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "Bahamut",
|
||||||
|
"outbounds": ["节点选择", "<all-country-tags>", "direct"],
|
||||||
|
"default": "节点选择",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "block",
|
||||||
|
"tag": "block"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"experimental": {
|
||||||
|
"cache_file": {
|
||||||
|
"enabled": true,
|
||||||
|
"store_rdrc": true
|
||||||
|
},
|
||||||
|
"clash_api": {
|
||||||
|
"default_mode": "Enhanced",
|
||||||
|
"external_controller": "127.0.0.1:9090",
|
||||||
|
"external_ui": "./ui",
|
||||||
|
"external_ui_download_detour": "节点选择"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user