Browse Source

[sub] json + fragment

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
03b7a34793

+ 105 - 0
sub/default.json

@@ -0,0 +1,105 @@
+{
+  "dns": {
+    "tag": "dns_out",
+    "queryStrategy": "UseIP",
+    "servers": [
+      {
+        "address": "8.8.8.8",
+        "skipFallback": false
+      }
+    ]
+  },
+  "inbounds": [
+    {
+      "port": 10808,
+      "protocol": "socks",
+      "settings": {
+        "auth": "noauth",
+        "udp": true,
+        "userLevel": 8
+      },
+      "sniffing": {
+        "destOverride": [
+          "http",
+          "tls",
+          "fakedns"
+        ],
+        "enabled": true
+      },
+      "tag": "socks"
+    },
+    {
+      "port": 10809,
+      "protocol": "http",
+      "settings": {
+        "userLevel": 8
+      },
+      "tag": "http"
+    }
+  ],
+  "log": {
+    "loglevel": "warning"
+  },
+  "outbounds": [
+    {
+      "tag": "direct",
+      "protocol": "freedom",
+      "settings": {
+        "domainStrategy": "UseIP"
+      }
+    },
+    {
+      "tag": "block",
+      "protocol": "blackhole",
+      "settings": {
+        "response": {
+          "type": "http"
+        }
+      }
+    }
+  ],
+  "policy": {
+    "levels": {
+      "8": {
+        "connIdle": 300,
+        "downlinkOnly": 1,
+        "handshake": 4,
+        "uplinkOnly": 1
+      }
+    },
+    "system": {
+      "statsOutboundUplink": true,
+      "statsOutboundDownlink": true
+    }
+  },
+  "routing": {
+    "domainStrategy": "AsIs",
+    "rules": [
+      {
+        "type": "field",
+        "network": "tcp,udp",
+        "balancerTag": "all"
+      }
+    ],
+    "balancers": [
+      {
+        "tag": "all",
+        "selector": [
+          "proxy"
+        ],
+        "strategy": {
+          "type": "leastPing"
+        }
+      }
+    ]
+  },
+  "observatory": {
+    "probeInterval": "5m",
+    "probeURL": "https://api.github.com/_private/browser/stats",
+    "subjectSelector": [
+      "proxy"
+    ],
+    "EnableConcurrency": true
+  },
+  "stats": {}
+}

+ 36 - 6
sub/sub.go

@@ -47,23 +47,53 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 	engine := gin.Default()
 
-	subPath, err := s.settingService.GetSubPath()
+	subDomain, err := s.settingService.GetSubDomain()
 	if err != nil {
 		return nil, err
 	}
 
-	subDomain, err := s.settingService.GetSubDomain()
+	if subDomain != "" {
+		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
+	}
+
+	LinksPath, err := s.settingService.GetSubPath()
 	if err != nil {
 		return nil, err
 	}
 
-	if subDomain != "" {
-		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
+	JsonPath, err := s.settingService.GetSubJsonPath()
+	if err != nil {
+		return nil, err
+	}
+
+	Encrypt, err := s.settingService.GetSubEncrypt()
+	if err != nil {
+		return nil, err
+	}
+
+	ShowInfo, err := s.settingService.GetSubShowInfo()
+	if err != nil {
+		return nil, err
+	}
+
+	RemarkModel, err := s.settingService.GetRemarkModel()
+	if err != nil {
+		RemarkModel = "-ieo"
+	}
+
+	SubUpdates, err := s.settingService.GetSubUpdates()
+	if err != nil {
+		SubUpdates = "10"
+	}
+
+	SubJsonFragment, err := s.settingService.GetSubJsonFragment()
+	if err != nil {
+		SubJsonFragment = ""
 	}
 
-	g := engine.Group(subPath)
+	g := engine.Group("/")
 
-	s.sub = NewSUBController(g)
+	s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
 
 	return engine, nil
 }

+ 55 - 14
sub/subController.go

@@ -3,34 +3,57 @@ package sub
 import (
 	"encoding/base64"
 	"strings"
-	"x-ui/web/service"
 
 	"github.com/gin-gonic/gin"
 )
 
 type SUBController struct {
-	subService     SubService
-	settingService service.SettingService
+	subPath        string
+	subJsonPath    string
+	subEncrypt     bool
+	updateInterval string
+
+	subService     *SubService
+	subJsonService *SubJsonService
 }
 
-func NewSUBController(g *gin.RouterGroup) *SUBController {
-	a := &SUBController{}
+func NewSUBController(
+	g *gin.RouterGroup,
+	subPath string,
+	jsonPath string,
+	encrypt bool,
+	showInfo bool,
+	rModel string,
+	update string,
+	jsonFragment string) *SUBController {
+
+	a := &SUBController{
+		subPath:        subPath,
+		subJsonPath:    jsonPath,
+		subEncrypt:     encrypt,
+		updateInterval: update,
+
+		subService:     NewSubService(showInfo, rModel),
+		subJsonService: NewSubJsonService(jsonFragment),
+	}
 	a.initRouter(g)
 	return a
 }
 
 func (a *SUBController) initRouter(g *gin.RouterGroup) {
-	g = g.Group("/")
+	gLink := g.Group(a.subPath)
+	gJson := g.Group(a.subJsonPath)
 
-	g.GET("/:subid", a.subs)
+	gLink.GET(":subid", a.subs)
+
+	gJson.GET(":subid", a.subJsons)
 }
 
 func (a *SUBController) subs(c *gin.Context) {
-	subEncrypt, _ := a.settingService.GetSubEncrypt()
-	subShowInfo, _ := a.settingService.GetSubShowInfo()
+	println(c.Request.Header["User-Agent"][0])
 	subId := c.Param("subid")
 	host := strings.Split(c.Request.Host, ":")[0]
-	subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
+	subs, header, err := a.subService.GetSubs(subId, host)
 	if err != nil || len(subs) == 0 {
 		c.String(400, "Error!")
 	} else {
@@ -40,14 +63,32 @@ func (a *SUBController) subs(c *gin.Context) {
 		}
 
 		// Add headers
-		c.Writer.Header().Set("Subscription-Userinfo", headers[0])
-		c.Writer.Header().Set("Profile-Update-Interval", headers[1])
-		c.Writer.Header().Set("Profile-Title", headers[2])
+		c.Writer.Header().Set("Subscription-Userinfo", header)
+		c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+		c.Writer.Header().Set("Profile-Title", subId)
 
-		if subEncrypt {
+		if a.subEncrypt {
 			c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
 		} else {
 			c.String(200, result)
 		}
 	}
 }
+
+func (a *SUBController) subJsons(c *gin.Context) {
+	println(c.Request.Header["User-Agent"][0])
+	subId := c.Param("subid")
+	host := strings.Split(c.Request.Host, ":")[0]
+	jsonSub, header, err := a.subJsonService.GetJson(subId, host)
+	if err != nil || len(jsonSub) == 0 {
+		c.String(400, "Error!")
+	} else {
+
+		// Add headers
+		c.Writer.Header().Set("Subscription-Userinfo", header)
+		c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+		c.Writer.Header().Set("Profile-Title", subId)
+
+		c.String(200, jsonSub)
+	}
+}

+ 355 - 0
sub/subJsonService.go

@@ -0,0 +1,355 @@
+package sub
+
+import (
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"x-ui/database/model"
+	"x-ui/logger"
+	"x-ui/util/json_util"
+	"x-ui/util/random"
+	"x-ui/web/service"
+	"x-ui/xray"
+)
+
+//go:embed default.json
+var defaultJson string
+
+type SubJsonService struct {
+	fragmanet string
+
+	inboundService service.InboundService
+	SubService
+}
+
+func NewSubJsonService(fragment string) *SubJsonService {
+	return &SubJsonService{
+		fragmanet: fragment,
+	}
+}
+
+func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
+	inbounds, err := s.SubService.getInboundsBySubId(subId)
+	if err != nil || len(inbounds) == 0 {
+		return "", "", err
+	}
+
+	var header string
+	var traffic xray.ClientTraffic
+	var clientTraffics []xray.ClientTraffic
+	var configJson map[string]interface{}
+	var defaultOutbounds []json_util.RawMessage
+
+	json.Unmarshal([]byte(defaultJson), &configJson)
+	if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
+		for _, defaultOutbound := range outboundSlices {
+			jsonBytes, _ := json.Marshal(defaultOutbound)
+			defaultOutbounds = append(defaultOutbounds, jsonBytes)
+		}
+	}
+
+	outbounds := []json_util.RawMessage{}
+	startIndex := 0
+	// Prepare Inbounds
+	for _, inbound := range inbounds {
+		clients, err := s.inboundService.GetClients(inbound)
+		if err != nil {
+			logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
+		}
+		if clients == nil {
+			continue
+		}
+		if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
+			listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
+			if err == nil {
+				inbound.Listen = listen
+				inbound.Port = port
+				inbound.StreamSettings = streamSettings
+			}
+		}
+
+		var subClients []model.Client
+		for _, client := range clients {
+			if client.Enable && client.SubID == subId {
+				subClients = append(subClients, client)
+				clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
+			}
+		}
+
+		outbound := s.getOutbound(inbound, subClients, host, startIndex)
+		if outbound != nil {
+			outbounds = append(outbounds, outbound...)
+			startIndex += len(outbound)
+		}
+	}
+
+	if len(outbounds) == 0 {
+		return "", "", nil
+	}
+
+	// Prepare statistics
+	for index, clientTraffic := range clientTraffics {
+		if index == 0 {
+			traffic.Up = clientTraffic.Up
+			traffic.Down = clientTraffic.Down
+			traffic.Total = clientTraffic.Total
+			if clientTraffic.ExpiryTime > 0 {
+				traffic.ExpiryTime = clientTraffic.ExpiryTime
+			}
+		} else {
+			traffic.Up += clientTraffic.Up
+			traffic.Down += clientTraffic.Down
+			if traffic.Total == 0 || clientTraffic.Total == 0 {
+				traffic.Total = 0
+			} else {
+				traffic.Total += clientTraffic.Total
+			}
+			if clientTraffic.ExpiryTime != traffic.ExpiryTime {
+				traffic.ExpiryTime = 0
+			}
+		}
+	}
+
+	if s.fragmanet != "" {
+		outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
+	}
+
+	// Combile outbounds
+	outbounds = append(outbounds, defaultOutbounds...)
+	configJson["outbounds"] = outbounds
+	finalJson, _ := json.MarshalIndent(configJson, "", "  ")
+
+	header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+	return string(finalJson), header, nil
+}
+
+func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage {
+	var newOutbounds []json_util.RawMessage
+	stream := s.streamData(inbound.StreamSettings)
+
+	externalProxies, ok := stream["externalProxy"].([]interface{})
+	if !ok || len(externalProxies) == 0 {
+		externalProxies = []interface{}{
+			map[string]interface{}{
+				"forceTls": "same",
+				"dest":     host,
+				"port":     float64(inbound.Port),
+			},
+		}
+	}
+
+	delete(stream, "externalProxy")
+
+	config_index := startIndex
+	for _, ep := range externalProxies {
+		extPrxy := ep.(map[string]interface{})
+		inbound.Listen = extPrxy["dest"].(string)
+		inbound.Port = int(extPrxy["port"].(float64))
+		newStream := stream
+		switch extPrxy["forceTls"].(string) {
+		case "tls":
+			if newStream["security"] != "tls" {
+				newStream["security"] = "tls"
+				newStream["tslSettings"] = map[string]interface{}{}
+			}
+		case "none":
+			if newStream["security"] != "none" {
+				newStream["security"] = "none"
+				delete(newStream, "tslSettings")
+			}
+		}
+		streamSettings, _ := json.MarshalIndent(newStream, "", "  ")
+		inbound.StreamSettings = string(streamSettings)
+
+		for _, client := range clients {
+			inbound.Tag = fmt.Sprintf("proxy_%d", config_index)
+			switch inbound.Protocol {
+			case "vmess", "vless":
+				newOutbounds = append(newOutbounds, s.genVnext(inbound, client))
+			case "trojan", "shadowsocks":
+				newOutbounds = append(newOutbounds, s.genServer(inbound, client))
+			}
+			config_index += 1
+		}
+	}
+
+	return newOutbounds
+}
+
+func (s *SubJsonService) streamData(stream string) map[string]interface{} {
+	var streamSettings map[string]interface{}
+	json.Unmarshal([]byte(stream), &streamSettings)
+	security, _ := streamSettings["security"].(string)
+	if security == "tls" {
+		streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
+	} else if security == "reality" {
+		streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
+	}
+	delete(streamSettings, "sockopt")
+
+	if s.fragmanet != "" {
+		streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "TcpNoDelay": true}`)
+	}
+
+	// remove proxy protocol
+	network, _ := streamSettings["network"].(string)
+	switch network {
+	case "tcp":
+		streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
+	case "ws":
+		streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
+	}
+
+	return streamSettings
+}
+
+func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
+	netSettings, ok := setting.(map[string]interface{})
+	if ok {
+		delete(netSettings, "acceptProxyProtocol")
+	}
+	return netSettings
+}
+
+func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
+	tlsData := make(map[string]interface{}, 1)
+	tlsClientSettings := tData["settings"].(map[string]interface{})
+
+	tlsData["serverName"] = tData["serverName"]
+	tlsData["alpn"] = tData["alpn"]
+	if allowInsecure, ok := tlsClientSettings["allowInsecure"].(string); ok {
+		tlsData["allowInsecure"] = allowInsecure
+	}
+	if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
+		tlsData["fingerprint"] = fingerprint
+	}
+	return tlsData
+}
+
+func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
+	rltyData := make(map[string]interface{}, 1)
+	rltyClientSettings := rData["settings"].(map[string]interface{})
+
+	rltyData["show"] = false
+	rltyData["publicKey"] = rltyClientSettings["publicKey"]
+	rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
+
+	// Set random data
+	rltyData["spiderX"] = "/" + random.Seq(15)
+	shortIds, ok := rData["shortIds"].([]interface{})
+	if ok && len(shortIds) > 0 {
+		rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
+	} else {
+		rltyData["shortId"] = ""
+	}
+	serverNames, ok := rData["serverNames"].([]interface{})
+	if ok && len(serverNames) > 0 {
+		rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
+	} else {
+		rltyData["serverName"] = ""
+	}
+
+	return rltyData
+}
+
+func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+	outbound := Outbound{}
+	usersData := make([]UserVnext, 1)
+
+	usersData[0].ID = client.ID
+	usersData[0].Level = 8
+	if inbound.Protocol == model.VLESS {
+		usersData[0].Flow = client.Flow
+		usersData[0].Encryption = "none"
+	}
+
+	vnextData := make([]VnextSetting, 1)
+	vnextData[0] = VnextSetting{
+		Address: inbound.Listen,
+		Port:    inbound.Port,
+		Users:   usersData,
+	}
+
+	outbound.Protocol = string(inbound.Protocol)
+	outbound.Tag = inbound.Tag
+	outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+	outbound.Settings = OutboundSettings{
+		Vnext: vnextData,
+	}
+
+	result, _ := json.MarshalIndent(outbound, "", "  ")
+	return result
+}
+
+func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+	outbound := Outbound{}
+
+	serverData := make([]ServerSetting, 1)
+	serverData[0] = ServerSetting{
+		Address:  inbound.Listen,
+		Port:     inbound.Port,
+		Level:    8,
+		Password: client.Password,
+	}
+
+	if inbound.Protocol == model.Shadowsocks {
+		var inboundSettings map[string]interface{}
+		json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
+		method, _ := inboundSettings["method"].(string)
+		serverData[0].Method = method
+
+		// server password in multi-user 2022 protocols
+		if strings.HasPrefix(method, "2022") {
+			if serverPassword, ok := inboundSettings["password"].(string); ok {
+				serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
+			}
+		}
+	}
+
+	outbound.Protocol = string(inbound.Protocol)
+	outbound.Tag = inbound.Tag
+	outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+	outbound.Settings = OutboundSettings{
+		Servers: serverData,
+	}
+
+	result, _ := json.MarshalIndent(outbound, "", "  ")
+	return result
+}
+
+type Outbound struct {
+	Protocol       string                 `json:"protocol"`
+	Tag            string                 `json:"tag"`
+	StreamSettings json_util.RawMessage   `json:"streamSettings"`
+	Mux            map[string]interface{} `json:"mux,omitempty"`
+	ProxySettings  map[string]interface{} `json:"proxySettings,omitempty"`
+	Settings       OutboundSettings       `json:"settings,omitempty"`
+}
+
+type OutboundSettings struct {
+	Vnext   []VnextSetting  `json:"vnext,omitempty"`
+	Servers []ServerSetting `json:"servers,omitempty"`
+}
+
+type VnextSetting struct {
+	Address string      `json:"address"`
+	Port    int         `json:"port"`
+	Users   []UserVnext `json:"users"`
+}
+
+type UserVnext struct {
+	Encryption string `json:"encryption,omitempty"`
+	Flow       string `json:"flow,omitempty"`
+	ID         string `json:"id"`
+	Level      int    `json:"level"`
+}
+
+type ServerSetting struct {
+	Password string `json:"password"`
+	Level    int    `json:"level"`
+	Address  string `json:"address"`
+	Port     int    `json:"port"`
+	Flow     string `json:"flow,omitempty"`
+	Method   string `json:"method,omitempty"`
+}

+ 36 - 31
sub/subService.go

@@ -25,47 +25,42 @@ type SubService struct {
 	settingService service.SettingService
 }
 
-func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
+func NewSubService(showInfo bool, remarkModel string) *SubService {
+	return &SubService{
+		showInfo:    showInfo,
+		remarkModel: remarkModel,
+	}
+}
+
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
 	s.address = host
-	s.showInfo = showInfo
 	var result []string
-	var headers []string
+	var header string
 	var traffic xray.ClientTraffic
 	var clientTraffics []xray.ClientTraffic
 	inbounds, err := s.getInboundsBySubId(subId)
 	if err != nil {
-		return nil, nil, err
+		return nil, "", err
 	}
-	s.remarkModel, err = s.settingService.GetRemarkModel()
+
+	s.datepicker, err = s.settingService.GetDatepicker()
 	if err != nil {
-		s.remarkModel = "-ieo"
+		s.datepicker = "gregorian"
 	}
-	s.datepicker, err = s.settingService.GetDatepicker()
-    if err != nil {
-        s.datepicker = "gregorian"
-    }
 	for _, inbound := range inbounds {
 		clients, err := s.inboundService.GetClients(inbound)
 		if err != nil {
-			logger.Error("SubService - GetSub: Unable to get clients from inbound")
+			logger.Error("SubService - GetClients: Unable to get clients from inbound")
 		}
 		if clients == nil {
 			continue
 		}
 		if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
-			fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
+			listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
 			if err == nil {
-				inbound.Listen = fallbackMaster.Listen
-				inbound.Port = fallbackMaster.Port
-				var stream map[string]interface{}
-				json.Unmarshal([]byte(inbound.StreamSettings), &stream)
-				var masterStream map[string]interface{}
-				json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
-				stream["security"] = masterStream["security"]
-				stream["tlsSettings"] = masterStream["tlsSettings"]
-				stream["externalProxy"] = masterStream["externalProxy"]
-				modifiedStream, _ := json.MarshalIndent(stream, "", "  ")
-				inbound.StreamSettings = string(modifiedStream)
+				inbound.Listen = listen
+				inbound.Port = port
+				inbound.StreamSettings = streamSettings
 			}
 		}
 		for _, client := range clients {
@@ -76,6 +71,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 			}
 		}
 	}
+
+	// Prepare statistics
 	for index, clientTraffic := range clientTraffics {
 		if index == 0 {
 			traffic.Up = clientTraffic.Up
@@ -97,11 +94,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
 			}
 		}
 	}
-	headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
-	updateInterval, _ := s.settingService.GetSubUpdates()
-	headers = append(headers, fmt.Sprintf("%d", updateInterval))
-	headers = append(headers, subId)
-	return result, headers, nil
+	header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+	return result, header, nil
 }
 
 func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -130,7 +124,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
 	return xray.ClientTraffic{}
 }
 
-func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
+func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
 	db := database.GetDB()
 	var inbound *model.Inbound
 	err := db.Model(model.Inbound{}).
@@ -138,9 +132,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
 		Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
 		Find(&inbound).Error
 	if err != nil {
-		return nil, err
+		return "", 0, "", err
 	}
-	return inbound, nil
+
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(streamSettings), &stream)
+	var masterStream map[string]interface{}
+	json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
+	stream["security"] = masterStream["security"]
+	stream["tlsSettings"] = masterStream["tlsSettings"]
+	stream["externalProxy"] = masterStream["externalProxy"]
+	modifiedStream, _ := json.MarshalIndent(stream, "", "  ")
+
+	return inbound.Listen, inbound.Port, string(modifiedStream), nil
 }
 
 func (s *SubService) getLink(inbound *model.Inbound, email string) string {
@@ -578,6 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 		if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
 			params["sni"], _ = sniValue.(string)
 		}
+
 		tlsSettings, _ := searchKey(tlsSetting, "settings")
 		if tlsSetting != nil {
 			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {

+ 3 - 0
web/assets/js/model/setting.js

@@ -28,6 +28,7 @@ class AllSetting {
         this.subListen = "";
         this.subPort = "2096";
         this.subPath = "/sub/";
+        this.subJsonPath = "/json/";
         this.subDomain = "";
         this.subCertFile = "";
         this.subKeyFile = "";
@@ -35,6 +36,8 @@ class AllSetting {
         this.subEncrypt = true;
         this.subShowInfo = false;
         this.subURI = '';
+        this.subJsonURI = '';
+        this.subJsonFragment = '';
 
         this.timeLocation = "Asia/Tehran";
 

+ 0 - 1
web/controller/xray_setting.go

@@ -81,7 +81,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
 		resp, err = a.XraySettingService.RegWarp(skey, pkey)
 	case "license":
 		license := c.PostForm("license")
-		println(license)
 		resp, err = a.XraySettingService.SetWarpLicence(license)
 	}
 

+ 10 - 0
web/entity/entity.go

@@ -48,6 +48,9 @@ type AllSetting struct {
 	SubEncrypt       bool   `json:"subEncrypt" form:"subEncrypt"`
 	SubShowInfo      bool   `json:"subShowInfo" form:"subShowInfo"`
 	SubURI           string `json:"subURI" form:"subURI"`
+	SubJsonPath      string `json:"subJsonPath" form:"subJsonPath"`
+	SubJsonURI       string `json:"subJsonURI" form:"subJsonURI"`
+	SubJsonFragment  string `json:"subJsonFragment" form:"subJsonFragment"`
 	Datepicker       string `json:"datepicker" form:"datepicker"`
 }
 
@@ -105,6 +108,13 @@ func (s *AllSetting) CheckValid() error {
 		s.SubPath += "/"
 	}
 
+	if !strings.HasPrefix(s.SubJsonPath, "/") {
+		s.SubJsonPath = "/" + s.SubJsonPath
+	}
+	if !strings.HasSuffix(s.SubJsonPath, "/") {
+		s.SubJsonPath += "/"
+	}
+
 	_, err := time.LoadLocation(s.TimeLocation)
 	if err != nil {
 		return common.NewError("time location not exist:", s.TimeLocation)

+ 19 - 5
web/html/common/qrcode_modal.html

@@ -8,13 +8,23 @@
         {{ i18n "pages.inbounds.clickOnQRcode" }}
     </a-tag>
     <template v-if="app.subSettings.enable && qrModal.subId">
-        <a-divider>Subscription</a-divider>
-        <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
+        <a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
+        <canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))"
+            id="qrCode-sub"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
+        </canvas>
+        <a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
+        <canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))"
+            id="qrCode-subJson"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
+        </canvas>
     </template>
     <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
     <template v-for="(row, index) in qrModal.qrcodes">
-        <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
-        <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
+        <a-tag color="blue" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
+        <canvas @click="copyToClipboard('qrCode-'+index, row.link)"
+            :id="'qrCode-'+index"
+            style="width: 100%; height: 100%; display: flex; border-radius: 1rem;"></canvas>
     </template>
 </a-modal>
 
@@ -82,12 +92,16 @@
             },
             genSubLink(subID) {
                 return app.subSettings.subURI+subID;
+            },
+            genSubJsonLink(subID) {
+                return app.subSettings.subJsonURI+subID;
             }
         },
         updated() {
             if (qrModal.client && qrModal.client.subId) {
                 qrModal.subId = qrModal.client.subId;
                 this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
+                this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
             }
             qrModal.qrcodes.forEach((element, index) => {
                 this.setQrCode("qrCode-" + index, element.link);
@@ -96,4 +110,4 @@
     });
 
 </script>
-{{end}}
+{{end}}

+ 16 - 1
web/html/xui/inbound_info_modal.html

@@ -166,7 +166,7 @@
         <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
             <a-divider>Subscription URL</a-divider>
             <a-row>
-                <a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
+                <a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
                 <a-col :sx="24" :md="2" style="text-align: right;">
                     <a-tooltip title='{{ i18n "copy" }}'>
                         <button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
@@ -175,6 +175,16 @@
                     </a-tooltip>
                 </a-col>
             </a-row>
+            <a-row>
+                <a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
+                <a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
+                    <a-tooltip title='{{ i18n "copy" }}'>
+                        <button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
+                            <a-icon type="snippets"></a-icon>
+                        </button>
+                    </a-tooltip>
+                </a-col>
+            </a-row>
         </template>
         <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
             <a-divider>Telegram ID</a-divider>
@@ -345,6 +355,7 @@
         index: null,
         isExpired: false,
         subLink: '',
+        subJsonLink: '',
         show(dbInbound, index) {
             this.index = index;
             this.inbound = dbInbound.toInbound();
@@ -360,6 +371,7 @@
             if (this.clientSettings) {
                 if (this.clientSettings.subId) {
                     this.subLink = this.genSubLink(this.clientSettings.subId);
+                    this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
                 }
             }
             this.visible = true;
@@ -369,6 +381,9 @@
         },
         genSubLink(subID) {
             return app.subSettings.subURI+subID;
+        },
+        genSubJsonLink(subID) {
+            return app.subSettings.subJsonURI+subID;
         }
     };
 

+ 9 - 14
web/html/xui/inbounds.html

@@ -64,11 +64,6 @@
                     >
                     </a-alert>
                 </transition>
-                <transition name="list" appear>
-                    <a-tag v-if="false" color="red" style="margin-bottom: 10px">
-                        Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
-                    </a-tag>
-                </transition>
                 <transition name="list" appear>
                     <a-card hoverable>
                         <a-row>
@@ -576,7 +571,8 @@
             refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
             subSettings: {
                 enable : false,
-                subURI : ''
+                subURI : '',
+                subJsonURI : '',
             },
             remarkModel: '-ieo',
             datepicker: 'gregorian',
@@ -623,7 +619,8 @@
                     this.tgBotEnable = tgBotEnable;
                     this.subSettings = {
                         enable : subEnable,
-                        subURI: subURI
+                        subURI: subURI,
+                        subJsonURI: subJsonURI
                     };
                     this.pageSize = pageSize;
                     this.remarkModel = remarkModel;
@@ -997,7 +994,7 @@
             },
             delInbound(dbInboundId) {
                 this.$confirm({
-                    title: '{{ i18n "pages.inbounds.deleteInbound"}}',
+                    title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
                     content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
                     class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "delete"}}',
@@ -1010,7 +1007,7 @@
                 clientId = this.getClientId(dbInbound.protocol, client);
                 if (confirmation){
                     this.$confirm({
-                        title: '{{ i18n "pages.inbounds.deleteClient"}}',
+                        title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
                         content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
                         class: themeSwitcher.currentTheme,
                         okText: '{{ i18n "delete"}}',
@@ -1301,7 +1298,7 @@
             pagination(obj){
                 if (this.pageSize > 0 && obj.length>this.pageSize) {
                     // Set page options based on object size
-                    sizeOptions = []
+                    sizeOptions = [];
                     for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
                         sizeOptions.push(i.toString());
                     }
@@ -1314,8 +1311,8 @@
                         position: 'bottom',
                         pageSize: this.pageSize,
                         pageSizeOptions: sizeOptions
-                    }
-                    return p
+                    };
+                    return p;
                 }
                 return false
             },
@@ -1369,7 +1366,6 @@
             }
         },
     });
-
 </script>
 
 {{template "inboundModal"}}
@@ -1379,6 +1375,5 @@
 {{template "inboundInfoModal"}}
 {{template "clientsModal"}}
 {{template "clientsBulkModal"}}
-
 </body>
 </html>

+ 86 - 10
web/html/xui/settings.html

@@ -83,7 +83,17 @@
                     show-icon closable
                     >
                     </a-alert>
-                </transition>
+                    <a-alert type="error" v-if="confAlerts.length>0" style="margin-bottom: 10px"
+                        message='{{ i18n "secAlertTitle" }}'
+                        color="red"
+                        show-icon closable
+                        >
+                        <template slot="description">
+                            {{ i18n "secAlertConf" }}
+                            <li v-for="a in confAlerts">- [[ a ]]</li>
+                        </template>
+                        </a-alert>
+                    </transition>
                 <a-space direction="vertical">
                     <a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
                         <a-row style="display: flex; flex-wrap: wrap; align-items: center;">
@@ -93,9 +103,7 @@
                                     <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
                                 </a-space>
                             </a-col>
-                            <a-col :xs="24" :sm="14">
-                                <template>
-                                    <div>
+                            <a-col :xs="24" :sm="16">
                                         <template>
                                             <div>
                                                 <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
@@ -104,8 +112,6 @@
                                                 message='{{ i18n "pages.settings.infoDesc" }}'
                                                 show-icon
                                                 >
-                                            </div>
-                                        </template>
                                     </div>
                                 </template>
                             </a-col>
@@ -173,7 +179,6 @@
                                         <a-col :lg="24" :xl="12">
                                             <a-list-item-meta title="Language" />
                                         </a-col>
-
                                         <a-col :lg="24" :xl="12">
                                             <template>
                                                 <a-select
@@ -243,7 +248,6 @@
                                 <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
                             </a-form>
                         </a-tab-pane>
-
                         <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
                             <a-list item-layout="horizontal">
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
@@ -266,8 +270,7 @@
                                                     ref="selectBotLang"
                                                     v-model="allSetting.tgLang"
                                                     :dropdown-class-name="themeSwitcher.currentTheme"
-                                                    style="width: 100%"
-                                                >
+                                                    style="width: 100%">
                                                     <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
                                                         <span role="img" :aria-label="l.name" v-text="l.icon"></span>
                                                         &nbsp;&nbsp;<span v-text="l.name"></span>
@@ -294,6 +297,17 @@
                                 <setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
                             </a-list>
                         </a-tab-pane>
+                        <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
+                            <a-list item-layout="horizontal">
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
+                                <setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
+                                <setting-list-item type="switch" title='{{ i18n "pages.settings.fragment"}}' desc='{{ i18n "pages.settings.fragmentDesc"}}' v-model="fragment"></setting-list-item>
+                                <template v-if="fragment">
+                                    <setting-list-item type="text" title='length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
+                                    <setting-list-item type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
+                                </template>
+                            </a-list>
+                        </a-tab-pane>
                     </a-tabs>
                 </a-space>
             </a-spin>
@@ -324,6 +338,24 @@
             remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
             datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
             remarkSample: '',
+            defaultFragment: {
+                tag: "fragment",
+                protocol: "freedom",
+                settings: {
+                    domainStrategy: "AsIs",
+                    fragment: {
+                        packets: "tlshello",
+                        length: "100-200",
+                        interval: "10-20"
+                    }
+                },
+                streamSettings: {
+                    sockopt: {
+                        tcpKeepAliveIdle: 100,
+                        TcpNoDelay: true
+                    }
+                }
+            },
             get remarkModel() {
                 rm = this.allSetting.remarkModel;
                 return rm.length>1 ? rm.substring(1).split('') : [];
@@ -452,6 +484,50 @@
                 }
             },
         },
+        computed: {
+            fragment: {
+                get: function() { return this.allSetting?.subJsonFragment != ""; },
+                set: function (v) {
+                    this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
+                }
+            },
+            fragmentLength: {
+                get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; },
+                set: function(v) {
+                    if (v != ""){
+                        newFragment = JSON.parse(this.allSetting.subJsonFragment);
+                        newFragment.settings.fragment.length = v;
+                        this.allSetting.subJsonFragment = JSON.stringify(newFragment);
+                    }
+                }
+            },
+            fragmentInterval: {
+                get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : ""; },
+                set: function(v) {
+                    if (v != ""){
+                        newFragment = JSON.parse(this.allSetting.subJsonFragment);
+                        newFragment.settings.fragment.interval = v;
+                        this.allSetting.subJsonFragment = JSON.stringify(newFragment);
+                    }
+                }
+            },
+            confAlerts: {
+                get: function() {
+                    if (!this.allSetting) return [];
+                    var alerts = []
+                    if (this.allSetting.port == 54321) alerts.push('{{ i18n "pages.settings.panelPort"}}');
+                    panelPath = window.location.pathname.split('/').length<4
+                    if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "pages.settings.panelSettings"}} {{ i18n "pages.settings.panelUrlPath"}}');
+                    if (this.allSetting.subEnable) {
+                        subPath = this.allSetting.subURI.length >0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
+                        if (subPath == '/sub/') alerts.push('{{ i18n "pages.settings.subSettings"}} {{ i18n "pages.settings.subPath"}}');
+                        subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
+                        if (subJsonPath == '/json/') alerts.push('JSON {{ i18n "pages.settings.subPath"}}');
+                    }
+                    return alerts
+                }
+            }
+        }, 
         async mounted() {
             if (window.location.protocol !== "https:") {
                 this.showAlert = true;

+ 26 - 19
web/service/setting.go

@@ -57,6 +57,9 @@ var defaultValueMap = map[string]string{
 	"subEncrypt":         "true",
 	"subShowInfo":        "true",
 	"subURI":             "",
+	"subJsonPath":        "/json/",
+	"subJsonURI":         "",
+	"subJsonFragment":    "",
 	"datepicker":         "gregorian",
 	"warp":               "",
 }
@@ -387,17 +390,11 @@ func (s *SettingService) GetSubPort() (int, error) {
 }
 
 func (s *SettingService) GetSubPath() (string, error) {
-	subPath, err := s.getString("subPath")
-	if err != nil {
-		return "", err
-	}
-	if !strings.HasPrefix(subPath, "/") {
-		subPath = "/" + subPath
-	}
-	if !strings.HasSuffix(subPath, "/") {
-		subPath += "/"
-	}
-	return subPath, nil
+	return s.getString("subPath")
+}
+
+func (s *SettingService) GetSubJsonPath() (string, error) {
+	return s.getString("subJsonPath")
 }
 
 func (s *SettingService) GetSubDomain() (string, error) {
@@ -412,8 +409,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
 	return s.getString("subKeyFile")
 }
 
-func (s *SettingService) GetSubUpdates() (int, error) {
-	return s.getInt("subUpdates")
+func (s *SettingService) GetSubUpdates() (string, error) {
+	return s.getString("subUpdates")
 }
 
 func (s *SettingService) GetSubEncrypt() (bool, error) {
@@ -432,6 +429,14 @@ func (s *SettingService) GetSubURI() (string, error) {
 	return s.getString("subURI")
 }
 
+func (s *SettingService) GetSubJsonURI() (string, error) {
+	return s.getString("subJsonURI")
+}
+
+func (s *SettingService) GetSubJsonFragment() (string, error) {
+	return s.getString("subJsonFragment")
+}
+
 func (s *SettingService) GetDatepicker() (string, error) {
 	return s.getString("datepicker")
 }
@@ -484,6 +489,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
 		"subEnable":   func() (interface{}, error) { return s.GetSubEnable() },
 		"subURI":      func() (interface{}, error) { return s.GetSubURI() },
+		"subJsonURI":  func() (interface{}, error) { return s.GetSubJsonURI() },
 		"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
 		"datepicker":  func() (interface{}, error) { return s.GetDatepicker() },
 	}
@@ -498,10 +504,11 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		result[key] = value
 	}
 
-	if result["subEnable"].(bool) && result["subURI"].(string) == "" {
+	if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
 		subURI := ""
 		subPort, _ := s.GetSubPort()
 		subPath, _ := s.GetSubPath()
+		subJsonPath, _ := s.GetSubJsonPath()
 		subDomain, _ := s.GetSubDomain()
 		subKeyFile, _ := s.GetSubKeyFile()
 		subCertFile, _ := s.GetSubCertFile()
@@ -522,12 +529,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
 		} else {
 			subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
 		}
-		if subPath[0] == byte('/') {
-			subURI += subPath
-		} else {
-			subURI += "/" + subPath
+		if result["subURI"].(string) == "" {
+			result["subURI"] = subURI + subPath
+		}
+		if result["subJsonURI"].(string) == "" {
+			result["subJsonURI"] = subURI + subJsonPath
 		}
-		result["subURI"] = subURI
 	}
 
 	return result, nil

+ 3 - 0
web/translation/translate.en_US.toml

@@ -54,6 +54,7 @@
 "security" = "Security"
 "secAlertTitle" = "Security Alert"
 "secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection."
+"secAlertConf" = "Certain configurations have been identified as susceptible to attacks, prompting immediate action to reinforce security protocols and safeguard against potential security breaches."
 
 [menu]
 "dashboard" = "Overview"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
 "subURI" = "Reverse Proxy URI"
 "subURIDesc" = "The URI path of the subscription URL for use behind proxies."
+"fragment" = "Fragmentation"
+"fragmentDesc" = "Enable fragmentation for TLS hello packet"
 
 [pages.xray]
 "title" = "Xray Configs"

+ 3 - 0
web/translation/translate.es_ES.toml

@@ -54,6 +54,7 @@
 "security" = "Seguridad"
 "secAlertTitle" = "Alerta de seguridad"
 "secAlertSsl" = "Esta conexión no es segura. Evite ingresar información confidencial hasta que TLS esté activado para la protección de datos."
+"secAlertConf" = "Se han identificado ciertas configuraciones como susceptibles a ataques, lo que genera acciones inmediatas para reforzar los protocolos de seguridad y proteger contra posibles violaciones de seguridad."
 
 [menu]
 "dashboard" = "Estado del Sistema"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
 "subURI" = "URI de proxy inverso"
 "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
+"fragment" = "Fragmentación"
+"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo TLS"
 
 [pages.xray]
 "title" = "Xray Configuración"

+ 3 - 0
web/translation/translate.fa_IR.toml

@@ -54,6 +54,7 @@
 "security" = "امنیت"
 "secAlertTitle" = "هشدار‌امنیتی"
 "secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
+"secAlertConf" = "پیکربندی‌های خاصی مستعد حملات سایبری شناسایی شده‌اند، اقدام فوری برای تقویت پروتکل‌های امنیتی و محافظت در برابر نقض‌های امنیتی لازم است"
 
 [menu]
 "dashboard" = "نمای کلی"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
 "subURI" = "پروکسی معکوس URI مسیر"
 "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
+"fragment" = "تکه‌تکه شدن"
+"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تی‌ال‌اس"
 
 [pages.xray]
 "title" = "پیکربندی ایکس‌ری"

+ 3 - 0
web/translation/translate.id_ID.toml

@@ -54,6 +54,7 @@
 "security" = "Keamanan"
 "secAlertTitle" = "Peringatan keamanan"
 "secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
+"secAlertConf" = "Konfigurasi tertentu telah diidentifikasi rentan terhadap serangan, sehingga mendorong tindakan segera untuk memperkuat protokol keamanan dan melindungi dari potensi pelanggaran keamanan."
 
 [menu]
 "dashboard" = "Ikhtisar"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
 "subURI" = "URI Proxy Terbalik"
 "subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
+"fragment" = "Fragmentasi"
+"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
 
 [pages.xray]
 "title" = "Konfigurasi Xray"

+ 3 - 0
web/translation/translate.ru_RU.toml

@@ -54,6 +54,7 @@
 "security" = "Безопасность"
 "secAlertTitle" = "Предупреждение системы безопасности"
 "secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
+"secAlertConf" = "Некоторые конфигурации были определены как уязвимые для атак, что требует немедленных действий по усилению протоколов безопасности и защите от потенциальных нарушений безопасности."
 
 [menu]
 "dashboard" = "Статус системы"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
 "subURI" = "URI обратного прокси"
 "subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
+"fragment" = "Фрагментация"
+"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
 
 [pages.xray]
 "title" = "Настройки Xray"

+ 3 - 0
web/translation/translate.vi_VN.toml

@@ -54,6 +54,7 @@
 "security" = "Bảo vệ"
 "secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
 "secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
+"secAlertConf" = "Một số cấu hình nhất định đã được xác định là dễ bị tấn công, thúc đẩy hành động ngay lập tức để củng cố các giao thức bảo mật và bảo vệ chống lại các vi phạm bảo mật tiềm ẩn."
 
 [menu]
 "dashboard" = "Trạng thái hệ thống"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
 "subURI" = "URI proxy trung gian"
 "subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
+"fragment" = "Sự phân mảnh"
+"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
 
 [pages.xray]
 "title" = "Cài đặt Xray"

+ 3 - 0
web/translation/translate.zh_Hans.toml

@@ -54,6 +54,7 @@
 "security" = "安全"
 "secAlertTitle" = "安全警报"
 "secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
+"secAlertConf" = "某些配置已被确定为容易受到攻击,促使立即采取行动以加强安全协议并防范潜在的安全漏洞。"
 
 [menu]
 "dashboard" = "系统状态"
@@ -304,6 +305,8 @@
 "subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
 "subURI" = "反向代理 URI"
 "subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
+"fragment" = "碎片"
+"fragmentDesc" = "启用 TLS hello 数据包分段"
 
 [pages.xray]
 "title" = "Xray 设置"

+ 2 - 1
xray/config.go

@@ -16,7 +16,8 @@ type Config struct {
 	API             json_util.RawMessage `json:"api"`
 	Stats           json_util.RawMessage `json:"stats"`
 	Reverse         json_util.RawMessage `json:"reverse"`
-	FakeDNS         json_util.RawMessage `json:"fakeDns"`
+	FakeDNS         json_util.RawMessage `json:"fakedns"`
+	Observatory     json_util.RawMessage `json:"observatory"`
 }
 
 func (c *Config) Equals(other *Config) bool {