mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-17 01:56:06 +00:00
feat: apply inbound/outbound/routing changes live via Xray gRPC API
Add a hot-apply layer that computes a diff between the old and new generated config and applies only the changed parts through the Xray gRPC HandlerService and RoutingService, avoiding a full process restart whenever possible. A restart is still performed when sections that have no reload API (log, dns, policy, observatory, ...) actually change. Key additions: - internal/xray/hot_diff.go: ComputeHotDiff with canonical-JSON comparison (sorted keys, null=absent, full number precision) so UI reformatting never triggers a spurious restart - internal/xray/api.go: AddOutbound/DelOutbound, ApplyRoutingConfig, GetBalancerInfo, SetBalancerTarget, TestRoute gRPC wrappers - internal/web/service/xray.go: tryHotApply, ensureAPIServices, GetBalancersStatus, OverrideBalancer, TestRoute service methods - internal/web/controller/xray_setting.go: balancerStatus, balancerOverride, routeTest API endpoints - frontend: BalancersTab live-status/override columns, RouteTester component, Restart button removed (Save now hot-applies) - balancer-helpers.ts: syncObservatories never creates observatory sections for random/roundRobin balancers (no reload API → restart) - i18n: balancerLive/Override/routeTester keys added to all 13 locales
This commit is contained in:
@@ -4,12 +4,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service/outbound"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -46,6 +48,9 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
g.POST("/testOutbound", a.testOutbound)
|
||||
g.POST("/balancerStatus", a.balancerStatus)
|
||||
g.POST("/balancerOverride", a.balancerOverride)
|
||||
g.POST("/routeTest", a.routeTest)
|
||||
|
||||
// Outbound subscription (remote outbound lists)
|
||||
g.GET("/outbound-subs", a.listOutboundSubs)
|
||||
@@ -120,7 +125,9 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
jsonObj(c, string(result), nil)
|
||||
}
|
||||
|
||||
// updateSetting updates the Xray configuration settings.
|
||||
// updateSetting updates the Xray configuration settings and applies them to
|
||||
// the running core right away — through the gRPC API when only inbounds,
|
||||
// outbounds or routing rules changed, with a process restart otherwise.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
|
||||
@@ -135,6 +142,13 @@ func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
// Only reconcile a running core; a manually stopped xray stays stopped.
|
||||
if a.XrayService.IsXrayRunning() {
|
||||
if err := a.XrayService.RestartXray(false); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||
}
|
||||
|
||||
@@ -272,6 +286,77 @@ func (a *XraySettingController) testOutbound(c *gin.Context) {
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// balancerStatus reports the live state (override + strategy picks) of the
|
||||
// balancer tags given as a comma-separated "tags" form field.
|
||||
func (a *XraySettingController) balancerStatus(c *gin.Context) {
|
||||
raw := c.PostForm("tags")
|
||||
var tags []string
|
||||
for _, tag := range strings.Split(raw, ",") {
|
||||
if tag = strings.TrimSpace(tag); tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
statuses, err := a.XrayService.GetBalancersStatus(tags)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
byTag := make(map[string]service.BalancerStatus, len(statuses))
|
||||
for _, status := range statuses {
|
||||
byTag[status.Tag] = status
|
||||
}
|
||||
jsonObj(c, byTag, nil)
|
||||
}
|
||||
|
||||
// balancerOverride forces a balancer to a specific outbound tag; an empty
|
||||
// "target" clears the override.
|
||||
func (a *XraySettingController) balancerOverride(c *gin.Context) {
|
||||
tag := c.PostForm("tag")
|
||||
if tag == "" {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("tag is required"))
|
||||
return
|
||||
}
|
||||
target := c.PostForm("target")
|
||||
if err := a.XrayService.OverrideBalancer(tag, target); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, "", nil)
|
||||
}
|
||||
|
||||
// routeTest asks the running core which outbound it would route a synthetic
|
||||
// connection to.
|
||||
func (a *XraySettingController) routeTest(c *gin.Context) {
|
||||
port := 0
|
||||
if portStr := c.PostForm("port"); portStr != "" {
|
||||
parsed, err := strconv.Atoi(portStr)
|
||||
if err != nil || parsed < 0 || parsed > 65535 {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("invalid port"))
|
||||
return
|
||||
}
|
||||
port = parsed
|
||||
}
|
||||
req := xray.RouteTestRequest{
|
||||
InboundTag: c.PostForm("inboundTag"),
|
||||
Domain: c.PostForm("domain"),
|
||||
IP: c.PostForm("ip"),
|
||||
Port: port,
|
||||
Network: c.PostForm("network"),
|
||||
Protocol: c.PostForm("protocol"),
|
||||
Email: c.PostForm("email"),
|
||||
}
|
||||
if req.Domain == "" && req.IP == "" {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("domain or ip is required"))
|
||||
return
|
||||
}
|
||||
result, err := a.XrayService.TestRoute(req)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
// --- Outbound Subscription handlers ---
|
||||
|
||||
func (a *XraySettingController) listOutboundSubs(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user