From 2bca38b3c5fc87ef579ffe658c98693796269774 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 02:34:48 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=B4=9F?= =?UTF-8?q?=E8=BD=BD=E5=9D=87=E8=A1=A1=E8=B7=AF=E7=94=B1=20Action=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增三种负载均衡 Action: - balancer: 基于域名匹配的负载均衡 - balancer_ip: 基于 IP 匹配的负载均衡 - default_balancer: 默认出站负载均衡(匹配所有流量) 支持的负载均衡策略: - random: 随机选择(默认) - roundrobin: 轮询 - leastping: 最低延迟(需要 Observatory) - leastload: 最低负载(需要 Observatory) --- core/balancer.go | 130 +++++++++++++++++++++++++++++++++++++++++ core/core.go | 6 +- core/custom.go | 116 ++++++++++++++++++++++++++++++++++-- core/distro/all/all.go | 2 +- 4 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 core/balancer.go diff --git a/core/balancer.go b/core/balancer.go new file mode 100644 index 00000000..5e5ca873 --- /dev/null +++ b/core/balancer.go @@ -0,0 +1,130 @@ +package core + +import ( + "encoding/json" + "fmt" + + "github.com/xtls/xray-core/core" + coreConf "github.com/xtls/xray-core/infra/conf" +) + +// BalancerActionValue 负载均衡 action_value 配置结构 +// +// 用于 balancer、balancer_ip、default_balancer 三种 Action +// +// 示例: +// +// { +// "tag": "proxy-lb", +// "outbounds": [ +// {"tag": "proxy-1", "protocol": "vmess", "settings": {...}}, +// {"tag": "proxy-2", "protocol": "trojan", "settings": {...}} +// ], +// "strategy": "leastping", +// "fallbackTag": "direct" +// } +type BalancerActionValue struct { + // Tag 负载均衡器标识(必填) + Tag string `json:"tag"` + // Outbounds 出站配置数组(必填),自动提取 tag 作为 selector + Outbounds []json.RawMessage `json:"outbounds"` + // Strategy 负载均衡策略: random(默认), roundrobin, leastping, leastload + Strategy string `json:"strategy"` + // FallbackTag 回退出站 tag,当所有出站不可用时使用 + FallbackTag string `json:"fallbackTag,omitempty"` +} + +// ParseBalancerConfig 解析负载均衡配置 +func ParseBalancerConfig(actionValue *string) (*BalancerActionValue, error) { + if actionValue == nil { + return nil, fmt.Errorf("action_value is nil") + } + var config BalancerActionValue + if err := json.Unmarshal([]byte(*actionValue), &config); err != nil { + return nil, fmt.Errorf("failed to parse balancer config: %w", err) + } + if config.Tag == "" { + return nil, fmt.Errorf("balancer tag is required") + } + if len(config.Outbounds) == 0 { + return nil, fmt.Errorf("balancer outbounds is required") + } + if config.Strategy == "" { + config.Strategy = "random" + } + return &config, nil +} + +// BuildBalancerOutbounds 构建出站配置 +func BuildBalancerOutbounds(config *BalancerActionValue, existingOutbounds []*core.OutboundHandlerConfig) ([]*core.OutboundHandlerConfig, error) { + var newOutbounds []*core.OutboundHandlerConfig + + for _, outboundRaw := range config.Outbounds { + outbound := &coreConf.OutboundDetourConfig{} + if err := json.Unmarshal(outboundRaw, outbound); err != nil { + return nil, fmt.Errorf("failed to parse outbound: %w", err) + } + if outbound.Tag == "" { + continue + } + if hasOutboundWithTag(existingOutbounds, outbound.Tag) { + continue + } + if hasOutboundWithTag(newOutbounds, outbound.Tag) { + continue + } + built, err := outbound.Build() + if err != nil { + return nil, fmt.Errorf("failed to build outbound %s: %w", outbound.Tag, err) + } + newOutbounds = append(newOutbounds, built) + } + + return newOutbounds, nil +} + +// GetSelectorTags 从 outbounds 提取所有 tag +func GetSelectorTags(config *BalancerActionValue) []string { + var tags []string + for _, outboundRaw := range config.Outbounds { + var outbound struct { + Tag string `json:"tag"` + } + if err := json.Unmarshal(outboundRaw, &outbound); err != nil { + continue + } + if outbound.Tag != "" { + tags = append(tags, outbound.Tag) + } + } + return tags +} + +// BuildBalancingRule 构建负载均衡规则(返回 coreConf.BalancingRule 供 RouterConfig 使用) +func BuildBalancingRule(config *BalancerActionValue) (*coreConf.BalancingRule, error) { + selectorTags := GetSelectorTags(config) + if len(selectorTags) == 0 { + return nil, fmt.Errorf("no valid selector tags found") + } + + confBalancer := &coreConf.BalancingRule{ + Tag: config.Tag, + Selectors: selectorTags, + FallbackTag: config.FallbackTag, + } + + switch config.Strategy { + case "random": + confBalancer.Strategy.Type = "random" + case "roundrobin": + confBalancer.Strategy.Type = "roundRobin" + case "leastping": + confBalancer.Strategy.Type = "leastPing" + case "leastload": + confBalancer.Strategy.Type = "leastLoad" + default: + confBalancer.Strategy.Type = "random" + } + + return confBalancer, nil +} diff --git a/core/core.go b/core/core.go index c9de9ee4..17d1fb38 100644 --- a/core/core.go +++ b/core/core.go @@ -86,7 +86,7 @@ func getCore(c *conf.Conf, infos []*panel.NodeInfo) *core.Instance { ErrorLog: c.LogConfig.Output, } // Custom config - dnsConfig, outBoundConfig, routeConfig, err := GetCustomConfig(infos) + dnsConfig, outBoundConfig, routeConfig, observatoryConfig, err := GetCustomConfig(infos) if err != nil { log.WithField("err", err).Panic("failed to build custom config") } @@ -121,6 +121,10 @@ func getCore(c *conf.Conf, infos []*panel.NodeInfo) *core.Instance { Inbound: inBoundConfig, Outbound: outBoundConfig, } + // Add Observatory config if present (for leastping/leastload strategies) + if observatoryConfig != nil { + config.App = append(config.App, serial.ToTypedMessage(observatoryConfig)) + } server, err := core.New(config) if err != nil { log.WithField("err", err).Panic("failed to create instance") diff --git a/core/custom.go b/core/custom.go index b11f7df1..097d40a3 100644 --- a/core/custom.go +++ b/core/custom.go @@ -7,6 +7,7 @@ import ( panel "github.com/wyx2685/v2node/api/v2board" "github.com/xtls/xray-core/app/dns" + "github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/router" xnet "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/core" @@ -42,7 +43,7 @@ func hasOutboundWithTag(list []*core.OutboundHandlerConfig, tag string) bool { return false } -func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHandlerConfig, *router.Config, error) { +func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHandlerConfig, *router.Config, *observatory.Config, error) { //dns queryStrategy := "UseIPv4v6" if !hasPublicIPv6() { @@ -63,8 +64,8 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand coreOutboundConfig := append([]*core.OutboundHandlerConfig{}, defaultoutbound) block, _ := buildBlockOutbound() coreOutboundConfig = append(coreOutboundConfig, block) - dns, _ := buildDnsOutbound() - coreOutboundConfig = append(coreOutboundConfig, dns) + dnsOut, _ := buildDnsOutbound() + coreOutboundConfig = append(coreOutboundConfig, dnsOut) //route domainStrategy := "AsIs" @@ -78,6 +79,9 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand DomainStrategy: &domainStrategy, } + // observatory tags for leastping/leastload strategies + var observatoryTags []string + for _, info := range infos { if len(info.Common.Routes) == 0 { continue @@ -223,6 +227,96 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand continue } coreOutboundConfig = append(coreOutboundConfig, custom_outbound) + case "balancer": + balancerConfig, err := ParseBalancerConfig(route.ActionValue) + if err != nil { + continue + } + // 构建出站配置 + newOutbounds, err := BuildBalancerOutbounds(balancerConfig, coreOutboundConfig) + if err != nil { + continue + } + coreOutboundConfig = append(coreOutboundConfig, newOutbounds...) + // 构建负载均衡规则 + balancingRule, err := BuildBalancingRule(balancerConfig) + if err != nil { + continue + } + coreRouterConfig.Balancers = append(coreRouterConfig.Balancers, balancingRule) + // 构建路由规则(域名匹配) + rule := map[string]interface{}{ + "inboundTag": info.Tag, + "domain": route.Match, + "balancerTag": balancerConfig.Tag, + } + rawRule, err := json.Marshal(rule) + if err != nil { + continue + } + coreRouterConfig.RuleList = append(coreRouterConfig.RuleList, rawRule) + // 收集 Observatory tags + if balancerConfig.Strategy == "leastping" || balancerConfig.Strategy == "leastload" { + observatoryTags = append(observatoryTags, GetSelectorTags(balancerConfig)...) + } + case "balancer_ip": + balancerConfig, err := ParseBalancerConfig(route.ActionValue) + if err != nil { + continue + } + newOutbounds, err := BuildBalancerOutbounds(balancerConfig, coreOutboundConfig) + if err != nil { + continue + } + coreOutboundConfig = append(coreOutboundConfig, newOutbounds...) + balancingRule, err := BuildBalancingRule(balancerConfig) + if err != nil { + continue + } + coreRouterConfig.Balancers = append(coreRouterConfig.Balancers, balancingRule) + // 构建路由规则(IP 匹配) + rule := map[string]interface{}{ + "inboundTag": info.Tag, + "ip": route.Match, + "balancerTag": balancerConfig.Tag, + } + rawRule, err := json.Marshal(rule) + if err != nil { + continue + } + coreRouterConfig.RuleList = append(coreRouterConfig.RuleList, rawRule) + if balancerConfig.Strategy == "leastping" || balancerConfig.Strategy == "leastload" { + observatoryTags = append(observatoryTags, GetSelectorTags(balancerConfig)...) + } + case "default_balancer": + balancerConfig, err := ParseBalancerConfig(route.ActionValue) + if err != nil { + continue + } + newOutbounds, err := BuildBalancerOutbounds(balancerConfig, coreOutboundConfig) + if err != nil { + continue + } + coreOutboundConfig = append(coreOutboundConfig, newOutbounds...) + balancingRule, err := BuildBalancingRule(balancerConfig) + if err != nil { + continue + } + coreRouterConfig.Balancers = append(coreRouterConfig.Balancers, balancingRule) + // 构建路由规则(所有流量) + rule := map[string]interface{}{ + "inboundTag": info.Tag, + "network": "tcp,udp", + "balancerTag": balancerConfig.Tag, + } + rawRule, err := json.Marshal(rule) + if err != nil { + continue + } + coreRouterConfig.RuleList = append(coreRouterConfig.RuleList, rawRule) + if balancerConfig.Strategy == "leastping" || balancerConfig.Strategy == "leastload" { + observatoryTags = append(observatoryTags, GetSelectorTags(balancerConfig)...) + } default: continue } @@ -230,11 +324,21 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand } DnsConfig, err := coreDnsConfig.Build() if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } RouterConfig, err := coreRouterConfig.Build() if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err + } + // 创建固定的 Observatory 配置 + var observatoryConfig *observatory.Config + if len(observatoryTags) > 0 { + observatoryConfig = &observatory.Config{ + SubjectSelector: observatoryTags, + ProbeUrl: "https://www.gstatic.com/generate_204", + ProbeInterval: 30, + EnableConcurrency: true, + } } - return DnsConfig, coreOutboundConfig, RouterConfig, nil + return DnsConfig, coreOutboundConfig, RouterConfig, observatoryConfig, nil } diff --git a/core/distro/all/all.go b/core/distro/all/all.go index db01618e..4b2315fb 100644 --- a/core/distro/all/all.go +++ b/core/distro/all/all.go @@ -31,7 +31,7 @@ import ( _ "github.com/xtls/xray-core/transport/internet/tagged/taggedimpl" // Developer preview features - //_ "github.com/xtls/xray-core/app/observatory" + _ "github.com/xtls/xray-core/app/observatory" // Inbound and outbound proxies. _ "github.com/xtls/xray-core/proxy/anytls" From da0b8002b1bf7159daa450bff6afd5d31f8b1425 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 02:55:34 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=20release=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=88=86=E6=94=AF=E4=B8=BA=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b3a8284..9bfc2c53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - master + - main paths: - "**/*.go" - "go.mod" From 5cbd88951c56e1c76b4be0a23824f6d5fe701a4d Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 02:58:05 +0800 Subject: [PATCH 3/9] =?UTF-8?q?chore:=20=E4=BF=AE=E6=94=B9=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=9C=B0=E5=9D=80=E4=B8=BA=20missish/v2node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- script/install.sh | 8 ++++---- script/v2node.sh | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e220458c..16067503 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A v2board backend base on moddified xray-core. ### 一键安装 ``` -wget -N https://raw.githubusercontent.com/wyx2685/v2node/master/script/install.sh && bash install.sh +wget -N https://raw.githubusercontent.com/missish/v2node/main/script/install.sh && bash install.sh ``` ## 构建 @@ -19,4 +19,4 @@ GOEXPERIMENT=jsonv2 go build -v -o build_assets/v2node -trimpath -ldflags "-X 'g ## Stars 增长记录 -[![Stargazers over time](https://starchart.cc/wyx2685/v2node.svg?variant=adaptive)](https://starchart.cc/wyx2685/v2node) +[![Stargazers over time](https://starchart.cc/missish/v2node.svg?variant=adaptive)](https://starchart.cc/missish/v2node) diff --git a/script/install.sh b/script/install.sh index ac68a79e..a0c70a68 100644 --- a/script/install.sh +++ b/script/install.sh @@ -267,13 +267,13 @@ install_v2node() { cd /usr/local/v2node/ if [[ -z "$version_param" ]] ; then - last_version=$(curl -Ls "https://api.github.com/repos/wyx2685/v2node/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + last_version=$(curl -Ls "https://api.github.com/repos/missish/v2node/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') if [[ ! -n "$last_version" ]]; then echo -e "${red}检测 v2node 版本失败,可能是超出 Github API 限制,请稍后再试,或手动指定 v2node 版本安装${plain}" exit 1 fi echo -e "${green}检测到最新版本:${last_version},开始安装...${plain}" - url="https://github.com/wyx2685/v2node/releases/download/${last_version}/v2node-linux-${arch}.zip" + url="https://github.com/missish/v2node/releases/download/${last_version}/v2node-linux-${arch}.zip" curl -sL "$url" | pv -s 30M -W -N "下载进度" > /usr/local/v2node/v2node-linux.zip if [[ $? -ne 0 ]]; then echo -e "${red}下载 v2node 失败,请确保你的服务器能够下载 Github 的文件${plain}" @@ -281,7 +281,7 @@ install_v2node() { fi else last_version=$version_param - url="https://github.com/wyx2685/v2node/releases/download/${last_version}/v2node-linux-${arch}.zip" + url="https://github.com/missish/v2node/releases/download/${last_version}/v2node-linux-${arch}.zip" curl -sL "$url" | pv -s 30M -W -N "下载进度" > /usr/local/v2node/v2node-linux.zip if [[ $? -ne 0 ]]; then echo -e "${red}下载 v2node $1 失败,请确保此版本存在${plain}" @@ -375,7 +375,7 @@ EOF fi - curl -o /usr/bin/v2node -Ls https://raw.githubusercontent.com/wyx2685/v2node/main/script/v2node.sh + curl -o /usr/bin/v2node -Ls https://raw.githubusercontent.com/missish/v2node/main/script/v2node.sh chmod +x /usr/bin/v2node cd $cur_dir diff --git a/script/v2node.sh b/script/v2node.sh index 63498d1a..6e166db8 100644 --- a/script/v2node.sh +++ b/script/v2node.sh @@ -107,7 +107,7 @@ before_show_menu() { } install() { - bash <(curl -Ls https://raw.githubusercontent.com/wyx2685/v2node/master/script/install.sh) + bash <(curl -Ls https://raw.githubusercontent.com/missish/v2node/main/script/install.sh) if [[ $? == 0 ]]; then if [[ $# == 0 ]]; then start @@ -123,7 +123,7 @@ update() { else version=$2 fi - bash <(curl -Ls https://raw.githubusercontent.com/wyx2685/v2node/master/script/install.sh) $version + bash <(curl -Ls https://raw.githubusercontent.com/missish/v2node/main/script/install.sh) $version if [[ $? == 0 ]]; then echo -e "${green}更新完成,已自动重启 v2node,请使用 v2node log 查看运行日志${plain}" exit @@ -307,7 +307,7 @@ show_log() { } update_shell() { - wget -O /usr/bin/v2node -N --no-check-certificate https://raw.githubusercontent.com/wyx2685/v2node/master/script/v2node.sh + wget -O /usr/bin/v2node -N --no-check-certificate https://raw.githubusercontent.com/missish/v2node/main/script/v2node.sh if [[ $? != 0 ]]; then echo "" echo -e "${red}下载脚本失败,请检查本机能否连接 Github${plain}" @@ -513,7 +513,7 @@ show_usage() { show_menu() { echo -e " ${green}v2node 后端管理脚本,${plain}${red}不适用于docker${plain} ---- https://github.com/wyx2685/v2node --- +--- https://github.com/missish/v2node --- ${green}0.${plain} 修改配置 ———————————————— ${green}1.${plain} 安装 v2node From 31c4aaa6c56ca632e85f6091b670c5d800a1bbc2 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 03:00:49 +0800 Subject: [PATCH 4/9] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20README=20?= =?UTF-8?q?=E6=A0=87=E6=98=8E=E4=BF=AE=E6=94=B9=E7=89=88=E5=B9=B6=E6=84=9F?= =?UTF-8?q?=E8=B0=A2=E5=8E=9F=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16067503..1251a38b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ -# v2node -A v2board backend base on moddified xray-core. +# v2node (修改版) + +基于 [wyx2685/v2node](https://github.com/wyx2685/v2node) 的自用修改版本。 + 一个基于修改版xray内核的V2board节点服务端。 -**注意: 本项目需要搭配[修改版V2board](https://github.com/wyx2685/v2board)** +## 感谢 + +- [wyx2685/v2node](https://github.com/wyx2685/v2node) - 原版项目 + +**注意: 本项目搭配自用的 Xboard 路由插件使用** ## 软件安装 @@ -13,7 +19,8 @@ wget -N https://raw.githubusercontent.com/missish/v2node/main/script/install.sh ``` ## 构建 -``` bash + +```bash GOEXPERIMENT=jsonv2 go build -v -o build_assets/v2node -trimpath -ldflags "-X 'github.com/wyx2685/v2node/cmd.version=$version' -s -w -buildid=" ``` From 39c547d31a8a266061bf56ca97d44ce12a2b3653 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 03:08:59 +0800 Subject: [PATCH 5/9] =?UTF-8?q?ci:=20=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=B8=8A=E6=B8=B8=E7=9A=84=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sync-upstream.yml | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..70cbf486 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,59 @@ +name: Sync Upstream + +on: + schedule: + # 每天 UTC 0:00 检查上游更新 + - cron: '0 0 * * *' + workflow_dispatch: + inputs: + sync_method: + description: '同步方式' + required: true + default: 'rebase' + type: choice + options: + - rebase + - merge + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Add upstream + run: | + git remote add upstream https://github.com/wyx2685/v2node.git + git fetch upstream + + - name: Check for updates + id: check + run: | + UPSTREAM_COMMITS=$(git rev-list HEAD..upstream/main --count) + echo "upstream_commits=$UPSTREAM_COMMITS" >> $GITHUB_OUTPUT + if [ "$UPSTREAM_COMMITS" -gt 0 ]; then + echo "有 $UPSTREAM_COMMITS 个新提交需要同步" + else + echo "上游没有新提交" + fi + + - name: Sync upstream (rebase) + if: steps.check.outputs.upstream_commits > 0 && (github.event.inputs.sync_method == 'rebase' || github.event_name == 'schedule') + run: | + git rebase upstream/main + git push --force-with-lease + + - name: Sync upstream (merge) + if: steps.check.outputs.upstream_commits > 0 && github.event.inputs.sync_method == 'merge' + run: | + git merge upstream/main -m "chore: 同步上游更新" + git push From 4034e19a619fbc695d2b8235b78db2cd27f0f953 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 03:15:19 +0800 Subject: [PATCH 6/9] =?UTF-8?q?Revert=20"ci:=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=90=8C=E6=AD=A5=E4=B8=8A=E6=B8=B8=E7=9A=84=20workfl?= =?UTF-8?q?ow"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 39c547d31a8a266061bf56ca97d44ce12a2b3653. --- .github/workflows/sync-upstream.yml | 59 ----------------------------- 1 file changed, 59 deletions(-) delete mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml deleted file mode 100644 index 70cbf486..00000000 --- a/.github/workflows/sync-upstream.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Sync Upstream - -on: - schedule: - # 每天 UTC 0:00 检查上游更新 - - cron: '0 0 * * *' - workflow_dispatch: - inputs: - sync_method: - description: '同步方式' - required: true - default: 'rebase' - type: choice - options: - - rebase - - merge - -jobs: - sync: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Add upstream - run: | - git remote add upstream https://github.com/wyx2685/v2node.git - git fetch upstream - - - name: Check for updates - id: check - run: | - UPSTREAM_COMMITS=$(git rev-list HEAD..upstream/main --count) - echo "upstream_commits=$UPSTREAM_COMMITS" >> $GITHUB_OUTPUT - if [ "$UPSTREAM_COMMITS" -gt 0 ]; then - echo "有 $UPSTREAM_COMMITS 个新提交需要同步" - else - echo "上游没有新提交" - fi - - - name: Sync upstream (rebase) - if: steps.check.outputs.upstream_commits > 0 && (github.event.inputs.sync_method == 'rebase' || github.event_name == 'schedule') - run: | - git rebase upstream/main - git push --force-with-lease - - - name: Sync upstream (merge) - if: steps.check.outputs.upstream_commits > 0 && github.event.inputs.sync_method == 'merge' - run: | - git merge upstream/main -m "chore: 同步上游更新" - git push From ca331c863fb16c0f174bf9aae3bcd62af5ee1e9d Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 06:08:16 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20selector=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=94=AF=E6=8C=81=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=20fallback=20=E5=87=BA=E7=AB=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/balancer.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/balancer.go b/core/balancer.go index 5e5ca873..bd0b064f 100644 --- a/core/balancer.go +++ b/core/balancer.go @@ -18,19 +18,23 @@ import ( // "tag": "proxy-lb", // "outbounds": [ // {"tag": "proxy-1", "protocol": "vmess", "settings": {...}}, -// {"tag": "proxy-2", "protocol": "trojan", "settings": {...}} +// {"tag": "proxy-2", "protocol": "trojan", "settings": {...}}, +// {"tag": "fallback-proxy", "protocol": "vmess", "settings": {...}} // ], +// "selector": ["proxy-1", "proxy-2"], // "strategy": "leastping", -// "fallbackTag": "direct" +// "fallbackTag": "fallback-proxy" // } type BalancerActionValue struct { // Tag 负载均衡器标识(必填) Tag string `json:"tag"` - // Outbounds 出站配置数组(必填),自动提取 tag 作为 selector + // Outbounds 出站配置数组(必填) Outbounds []json.RawMessage `json:"outbounds"` + // Selector 参与负载均衡的出站 tag 列表(可选,默认使用所有 outbounds) + Selector []string `json:"selector,omitempty"` // Strategy 负载均衡策略: random(默认), roundrobin, leastping, leastload Strategy string `json:"strategy"` - // FallbackTag 回退出站 tag,当所有出站不可用时使用 + // FallbackTag 回退出站 tag,当所有出站不可用时使用(可以是不在 selector 中的出站) FallbackTag string `json:"fallbackTag,omitempty"` } @@ -83,8 +87,14 @@ func BuildBalancerOutbounds(config *BalancerActionValue, existingOutbounds []*co return newOutbounds, nil } -// GetSelectorTags 从 outbounds 提取所有 tag +// GetSelectorTags 获取参与负载均衡的 tag 列表 +// 如果设置了 Selector 则使用 Selector,否则使用所有 outbounds 的 tag func GetSelectorTags(config *BalancerActionValue) []string { + // 优先使用 Selector 字段 + if len(config.Selector) > 0 { + return config.Selector + } + // 否则从 outbounds 提取所有 tag var tags []string for _, outboundRaw := range config.Outbounds { var outbound struct { From 7d497b46ff28bf2d79085c3c3964613e34e3ea6f Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 06:57:07 +0800 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E8=A7=84=E5=88=99=E4=BC=98=E5=85=88=E7=BA=A7=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 getActionPriority 函数定义 action 优先级 - 新增 sortRoutesByPriority 函数按优先级排序路由规则 - 优先级顺序: dns → block/protocol → block_ip/block_port → balancer/route → balancer_ip/route_ip → default_out/default_balancer - 使用稳定排序保持同级规则的原始配置顺序 --- core/custom.go | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/core/custom.go b/core/custom.go index 097d40a3..64923705 100644 --- a/core/custom.go +++ b/core/custom.go @@ -3,6 +3,7 @@ package core import ( "encoding/json" "net" + "sort" "strings" panel "github.com/wyx2685/v2node/api/v2board" @@ -43,6 +44,43 @@ func hasOutboundWithTag(list []*core.OutboundHandlerConfig, tag string) bool { return false } +// getActionPriority 返回 action 的优先级,数字越小优先级越高 +// 优先级设计: +// 0: dns - DNS 配置(不生成路由规则) +// 1: block, protocol - 域名/协议阻止(最高优先级阻止) +// 2: block_ip, block_port - IP/端口阻止 +// 3: balancer, route - 域名匹配的负载均衡/路由(同级,保持配置顺序) +// 4: balancer_ip, route_ip - IP 匹配的负载均衡/路由(同级,保持配置顺序) +// 5: default_out, default_balancer - 兜底规则(最低优先级) +func getActionPriority(action string) int { + switch action { + case "dns": + return 0 + case "block", "protocol": + return 1 + case "block_ip", "block_port": + return 2 + case "balancer", "route": + return 3 + case "balancer_ip", "route_ip": + return 4 + case "default_out", "default_balancer": + return 5 + default: + return 6 + } +} + +// sortRoutesByPriority 按优先级排序路由规则 +func sortRoutesByPriority(routes []panel.Route) []panel.Route { + sorted := make([]panel.Route, len(routes)) + copy(sorted, routes) + sort.SliceStable(sorted, func(i, j int) bool { + return getActionPriority(sorted[i].Action) < getActionPriority(sorted[j].Action) + }) + return sorted +} + func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHandlerConfig, *router.Config, *observatory.Config, error) { //dns queryStrategy := "UseIPv4v6" @@ -86,7 +124,9 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand if len(info.Common.Routes) == 0 { continue } - for _, route := range info.Common.Routes { + // 按优先级排序路由规则 + sortedRoutes := sortRoutesByPriority(info.Common.Routes) + for _, route := range sortedRoutes { switch route.Action { case "dns": if route.ActionValue == nil { From c6fe6578e9e54caf423fd407a21fa366c9fa30a7 Mon Sep 17 00:00:00 2001 From: missish <2445921920@qq.com> Date: Wed, 28 Jan 2026 13:24:54 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Observatory=20P?= =?UTF-8?q?robeInterval=20=E5=8D=95=E4=BD=8D=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProbeInterval 单位是纳秒,之前设置 30 实际是 30 纳秒 - 修改为 10 * time.Second (10秒) --- core/custom.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/core/custom.go b/core/custom.go index 64923705..1908c232 100644 --- a/core/custom.go +++ b/core/custom.go @@ -5,6 +5,7 @@ import ( "net" "sort" "strings" + "time" panel "github.com/wyx2685/v2node/api/v2board" "github.com/xtls/xray-core/app/dns" @@ -46,12 +47,13 @@ func hasOutboundWithTag(list []*core.OutboundHandlerConfig, tag string) bool { // getActionPriority 返回 action 的优先级,数字越小优先级越高 // 优先级设计: -// 0: dns - DNS 配置(不生成路由规则) -// 1: block, protocol - 域名/协议阻止(最高优先级阻止) -// 2: block_ip, block_port - IP/端口阻止 -// 3: balancer, route - 域名匹配的负载均衡/路由(同级,保持配置顺序) -// 4: balancer_ip, route_ip - IP 匹配的负载均衡/路由(同级,保持配置顺序) -// 5: default_out, default_balancer - 兜底规则(最低优先级) +// +// 0: dns - DNS 配置(不生成路由规则) +// 1: block, protocol - 域名/协议阻止(最高优先级阻止) +// 2: block_ip, block_port - IP/端口阻止 +// 3: balancer, route - 域名匹配的负载均衡/路由(同级,保持配置顺序) +// 4: balancer_ip, route_ip - IP 匹配的负载均衡/路由(同级,保持配置顺序) +// 5: default_out, default_balancer - 兜底规则(最低优先级) func getActionPriority(action string) int { switch action { case "dns": @@ -376,7 +378,7 @@ func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHand observatoryConfig = &observatory.Config{ SubjectSelector: observatoryTags, ProbeUrl: "https://www.gstatic.com/generate_204", - ProbeInterval: 30, + ProbeInterval: int64(10 * time.Second), // 10秒,time.Duration 单位是纳秒 EnableConcurrency: true, } }