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" diff --git a/README.md b/README.md index e220458c..1251a38b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ -# 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 路由插件使用** ## 软件安装 ### 一键安装 ``` -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 ``` ## 构建 -``` bash + +```bash GOEXPERIMENT=jsonv2 go build -v -o build_assets/v2node -trimpath -ldflags "-X 'github.com/wyx2685/v2node/cmd.version=$version' -s -w -buildid=" ``` ## 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/core/balancer.go b/core/balancer.go new file mode 100644 index 00000000..bd0b064f --- /dev/null +++ b/core/balancer.go @@ -0,0 +1,140 @@ +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": {...}}, +// {"tag": "fallback-proxy", "protocol": "vmess", "settings": {...}} +// ], +// "selector": ["proxy-1", "proxy-2"], +// "strategy": "leastping", +// "fallbackTag": "fallback-proxy" +// } +type BalancerActionValue struct { + // Tag 负载均衡器标识(必填) + Tag string `json:"tag"` + // 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,当所有出站不可用时使用(可以是不在 selector 中的出站) + 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 获取参与负载均衡的 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 { + 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..1908c232 100644 --- a/core/custom.go +++ b/core/custom.go @@ -3,10 +3,13 @@ package core import ( "encoding/json" "net" + "sort" "strings" + "time" 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 +45,45 @@ func hasOutboundWithTag(list []*core.OutboundHandlerConfig, tag string) bool { return false } -func GetCustomConfig(infos []*panel.NodeInfo) (*dns.Config, []*core.OutboundHandlerConfig, *router.Config, error) { +// 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" if !hasPublicIPv6() { @@ -63,8 +104,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,11 +119,16 @@ 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 } - for _, route := range info.Common.Routes { + // 按优先级排序路由规则 + sortedRoutes := sortRoutesByPriority(info.Common.Routes) + for _, route := range sortedRoutes { switch route.Action { case "dns": if route.ActionValue == nil { @@ -223,6 +269,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 +366,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: int64(10 * time.Second), // 10秒,time.Duration 单位是纳秒 + 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" 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